[—ATOC—] 1 [—AUTO_SECTION_NUMBER—] 1
最初の実装
ここまでで,
- 画像を出力する方法
- レイと幾何物体の交差判定
を説明した.本章ではいよいよレイトレーシング法の実装を始める.
処理手順
基本的な処理手順に関しては1-1.1. アルゴリズムを参照せよ.
最初の実装では簡単のため陰影付けを行わない.また,物体はただ一つの球に限定する.
そしてレイが球と交点を持つならば赤,交点を持たないならば青を出力することとする.
したがって,処理手順は以下のように簡略化される.
- 視点の位置を決める
- スクリーン上の各点(x,y)について以下の処理を行う.
- 視点位置から点(x,y)に向かってレイを飛ばす.
- そのレイと球が交差するかどうかを調べる.
- 球と交差しない場合,青を出力する.
- 球と交差する場合,赤を出力する.
スクリーン座標の変換
最終的な出力を得るにはX座標0~(画像幅-1),Y座標0~(画像高さ-1)のすべての座標について処理をして,画像の全ての画素を埋める必要がある.
#define WIDTH /* 画像の幅 */ #define HIEGHT /* 画像の高さ */ void raytracing() { int x,y; for ( y = 0; y < HEIGHT; ++y ) { for( x = 0; x < WIDTH; ++x ) { /* 画素値ごとの処理 */ }//for }//for }//void raytracing() [/c]</div> <p>簡易化した処理手順を再掲する.</p> <ol> <li>視点の位置を決める</li> <li>スクリーン上の各点(x,y)について以下の処理を行う. <ol> <li><span style="color:red;font-weight:bold">視点位置から点(x,y)に向かってレイを飛ばす.</span></li> <li>そのレイと球が交差するかどうかを調べる. <ol> <li>球と交差しない場合,<strong>青を出力する</strong>.</li> <li>球と交差する場合,<strong>赤を出力する</strong>.</li> </ol></li> </ol></li> </ol> <p>赤字部分に着目する.<br /> <span style="color:red;font-weight:bold">視点位置</span>は三次元空間上の位置である.<br /> <span style="color:red;font-weight:bold">点(x,y)</span>は,<strong>三次元空間上に置いたスクリーン(画像)中の特定の位置</strong>である.<br /></p> <p>手順「そのレイと球が交差するかどうかを調べる」では,<strong>視点位置を始点とし,始点から点(x,y)への方向を方向ベクトルとするレイ</strong>と球との交差判定を行う.</p> <p>実際に画像のすべての画素を埋めていく際に扱う座標は,<strong>画像の左上を(0,0)とするスクリーン座標</strong>なので, これを三次元空間上の座標にするためには<strong>変換を行う必要がある</strong>.</p> <p>今回は三次元空間上のスクリーンを,<strong>左上(-1,1,0),右上(1,1,0),右下(1,-1,0),左下(-1,-1,0)</strong>とした幅2.0,高さ2.0の矩形とする(図1).</p> <p><img src="wp-content/uploads/2012/05/coordinate_convertion.png" alt="スクリーン座標の変換"><br /> <em>図1. スクリーン座標の変換</em></p> <p>したがって,スクリーン座標$$(x_{s},y_{s})$$から三次元座標$$(x_{w},y_{w},z_{w})$$への変換は各々以下のようになる.</p> <div>\(\left\{\begin{array}{lll} \\ x_{w} & = & \frac{2x_{s}}{W-1}-1.0 \\ y_{w} & = & -\frac{2y_{s}}{H-1}+1.0 \\ z_{w} & = & 0 \\ \end{array}\right.\)</div> <p><br /></p> <p>$$W,H$$は,それぞれ画像の幅と高さである.</p> <p>また,視点位置を$$\vec{\bf p_{\cal e}}$$とし,スクリーン座標の三次元空間上の位置を$$\vec{\bf p_{\cal w}}$$とすると,レイの方向ベクトル$$\vec{\bf d_{\cal e}}$$は以下となる.</p> <p>$$\vec{\bf d_{\cal e}}=\vec{\bf p_{\cal w}}-\vec{\bf p_{\cal e}}$$</p> <h3>シーン設定</h3> <p>想定するシーン(視点位置や物体の位置)は以下とする(図2).</p> <ul> <li>視点位置 <ul> <li>$$\vec{\bf p_{\cal e}}=(0,0,-5)$$</li> </ul></li> <li>スクリーン位置 <ul> <li>左上$$(-1,1,0)$$, 右上$$(1,1,0)$$, 右下$$(1,-1,0)$$, 左下$$(-1,-1,0)$$</li> </ul></li> <li>球 <ul> <li>中心位置$$\vec{\bf p_{\cal c}}=(0,0,5)$$</li> <li>半径$$radius=1.0$$</li> </ul></li> </ul> <p><img src="wp-content/uploads/2012/05/initial_scene.png" alt="シーン設定"><br /> <em>図2. シーン設定</em></p> <h3>プログラミングの前に</h3> <p>後述の,<a href="#toc-10dd900c7a4bb3abe0e761abd8c120fc">課題1-7 レイと球の交差判定</a>で実際にプログラムを作り始めるが, その前にC言語でベクトルを扱う方法を次節にて解説する.</p> <h2>ベクトルの扱い</h2> <p>C言語は様々な算術型をサポートしているがそのなかに三次元のベクトル型は存在しない.<br /> そこで以下のような構造体をベクトルとして使用する小さなライブラリを用意した.</p> <div> typedef struct { float x,y,z; } vector_t;
以下をダウンロードして使用すること.
アーカイブの展開方法は以下である.
アーカイブには
- vector_utils.h
- vector_utils.c
の二つのファイルが含まれている.
自分のプログラムで使用する際は,これらのファイルをメインの.cファイルと同じディレクトリにコピーすること.
初期化
vector_tは以下のようにして初期化する.
void func() { vector_t vec1 = {0.0f, 1.0f , 0.0f}; vector_t vec2 = {1.0f, 0.0f , 0.0f}; }
加減算とスカラー倍
ベクトル同士の加減算,スカラー倍を行うときは以下のようにxyz成分ごとに 同じ計算式を記述する.
void func() { vector_t vec1 = {0.0f, 1.0f , 0.0f}; vector_t vec2 = {1.0f, 0.0f , 0.0f}; vector_t vec3; /* 加算 ... 意味的には vec3 = vec1 + vec2 */ vec3.x = vec1.x + vec2.x; vec3.y = vec1.y + vec2.y; vec3.z = vec1.z + vec2.z; /* 減算 ... 意味的には vec3 = vec1 - vec2 */ vec3.x = vec1.x - vec2.x; vec3.y = vec1.y - vec2.y; vec3.z = vec1.z - vec2.z; /* スカラー倍と減算 ... 意味的には vec3 = vec1 + 10*vec2 */ vec3.x = vec1.x - 10*vec2.x; vec3.y = vec1.y - 10*vec2.y; vec3.z = vec1.z - 10*vec2.z; }
内積,外積,ノルム(長さ)
ベクトルの内積,外積,ノルム(長さ),正規化に関しては以下のような関数を用意してある.
内積
関数dotは戻り値としてふたつのベクトルの内積を返す.
$$\left(\vec{\bf a}\cdot\vec{\bf b}\right)$$
float dot(const vector_t* a, const vector_t* b);
以下のようにして使う.
void func() { vector_t v1 = { /* ...省略... */ }; vector_t v2 = { /* ...省略... */ }; float v1v2_dot; v1v2_dot = dot(&v1, &v2); /* 内積を計算したい二つのベクトルのポインタを渡す */ }
外積
関数crossは二つのベクトルの外積を第一引数のベクトルに格納する.
$$\vec{\bf o} \leftarrow \left(\vec{\bf a}\times\vec{\bf b}\right)$$
void cross(vector_t *o, const vector_t* a, const vector_t* b);
以下のようにして使う.
void func() { vector_t v1 = { /* ...省略... */ }; vector_t v2 = { /* ...省略... */ }; vector_t o; cross(&o, &v1, &v2); /* ベクトルv1,v2の外積がベクトルoに入る */ }
ノルム
関数norm, squared_normは戻り値として,それぞれベクトルのノルム(長さ)と,ベクトルのノルムの二乗を返す.
$$|\vec{\bf v}|$$
float norm(const vector_t*v)
$$|\vec{\bf v}|^{2}$$
float squared_norm(const vector_t*v)
以下のようにして使う.
void func() { vector_t v1 = { /* ...省略... */ }; float hoge; float piyo; hoge = norm(&v1); /* ベクトルv1のノルムが返る */ piyo = squared_norm(&v1); /* ベクトルv1のノルム^2が返る */ }
正規化
関数normalizeはベクトルを正規化(ノルムを1にする).戻り値として正規化前のノルムを返す(無視しても良い).
float normalize(vector_t* v);
以下のようにして使う.
void func() { vector_t v1 = { /* ...省略... */ }; normalize(&v1); /* ベクトルv1が正規化される */ }
ベクトルの表示
関数vector_strは,ベクトルを文字列化する.
const char* vector_str(const vector_t*v);
リスト8のようにして使用する.
vector_strは文字列を返すのでprintf系関数の書式指定子は%s(文字列)となることに注意せよ.
void func() { vector_t vec1 = {1.0f, 2.0f, 3.0f}; printf("vec1 = %s\n", vector_str(&vec1)); }
結果は以下のようになる.
そのほか(コンビニエンスマクロ)
そのほか以下の便利関数(マクロ)を含めてある.
/* xの二乗 */ #define SQR(x) ((x)*(x)) /* a,bのうち小さい方を返す */ #define MIN(a,b) (a < b ? a : b) /* a,bのうち大きい方を返す */ #define MAX(a,b) (a > b ? a : b) /* 値vを範囲[minv,maxv]に切り詰める */ #define CLAMP(v,minv,maxv) MIN(MAX(v,minv),maxv)
以下のようにして使う.
void func() { float hoge, piyo, foo, bar, fuga, buzz; hoge = SQR(3.14f); /* 3.14の二乗 */ piyo = 2.71828f; foo = MIN(hoge, piyo); /* hogeとpiyoのうち小さい方がfooに入る */ bar = MAX(hoge, piyo); /* hogeとpiyoのうち大きい方がbarに入る */ fuga = 1.41421356f; buzz = CLAMP(fuga, 0.0f, 1.0f); /* fugaの値は1.0以上なので1.0が返る */ fuga = -100.0f; buzz = CLAMP(fuga, 0.0f, 1.0f); /* fugaの値は0.0以下なので0.0が返る */ fuga = 0.5f; buzz = CLAMP(fuga, 0.0f, 1.0f); /* fugaの値は[0,1]の範囲内なので0.5が返る */ }
課題 [—EXCLUDE—]
課題1-6 ベクトルを扱う練習
(作業時間目安:10分)
二つのベクトル$$\overrightarrow{\bf vec_{1}}, \overrightarrow{\bf vec_{2}}$$を定義し,以下の計算結果を表示せよ.
- $$\overrightarrow{\bf vec_{1}}$$の値
- $$\overrightarrow{\bf vec_{2}}$$の値
- $$\overrightarrow{\bf vec_{1}} + \overrightarrow{\bf vec_{2}}$$の計算結果
- $$3\overrightarrow{\bf vec_{1}} – 4\overrightarrow{\bf vec_{2}}$$の計算結果
- $$3\overrightarrow{\bf vec_{1}} – 4\overrightarrow{\bf vec_{2}}$$を正規化した値
- $$\left(\overrightarrow{\bf vec_{1}}\cdot\overrightarrow{\bf vec_{2}}\right)$$の計算結果
- $$\left(\overrightarrow{\bf vec_{1}}\times\overrightarrow{\bf vec_{2}}\right)$$の計算結果
$$\overrightarrow{\bf vec_{1}}, \overrightarrow{\bf vec_{2}}$$の初期値は以下である.
ソースファイル名はtest_vector.cとする.
なお以下の実行結果のように整形して出力せよ.
プログラムのビルド方法および実行方法
以下のコマンドでプログラムの実行形式を生成する.
問題なくビルドできたら,以下のコマンドで実行する.
課題1-7 レイと球の交差判定
(作業時間目安:40分)
4-1. 最初の実装で示したレイと球の交差判定を行うプログラムを作成せよ.
レイと球が交差する場合は赤を出力し,交差しない場合は青を出力すること.
シーンの設定は4-1.3. シーン設定に従う.
ソースファイル名はfirst_sample.cとする.
画像サイズは幅512ピクセル,高さ512ピクセルとせよ.
生成画像は図3のようになる.
図3. first_sample.cの生成画像
この問題が難しいと感じたら課題1-7補足問題をやってみてください(補足問題すべてに取り組む必要はありません).
プログラムのビルド方法および実行方法
以下のコマンドでプログラムの実行形式を生成する.
問題なくビルドできたら,以下のコマンドでPPM形式画像を生成し表示する.
ヒント1
ヒント2
- スクリーン座標$$y$$を$$[0,H)$$の範囲でループする.
- スクリーン座標$$y$$を三次元空間座標$$y_{w}$$に変換する.
- スクリーン座標$$x$$を$$[0,W)$$の範囲でループする.
- スクリーン座標$$x$$を三次元空間座標$$x_{w}$$に変換する.
- 視線ベクトル$$\vec{\bf d_{\cal e}}$$を計算する.
- $$\vec{\bf d_{\cal e}}=\vec{\bf p_{\cal w}}-\vec{\bf p_{\cal e}}$$
- 交点計算で必要なので,$$\overrightarrow{\bf v_{\cal tmp}}=\left(\vec{\bf p_{\cal e}}-\vec{\bf p_{\cal c}}\right)$$を計算しておく.
- 交点計算のため二次方程式$$Ax^{2}+Bx+C=0$$の係数$$A,B,C$$を計算する.
- $$A=\left|\vec{\bf d_{\cal e}}\right|^{2}$$
- $$B=2\left(\vec{\bf d_{\cal e}}\cdot\overrightarrow{\bf v_{\cal tmp}}\right)$$
- $$C=\left|\overrightarrow{\bf v_{\cal tmp}}\right|^{2}-radius^{2}$$
- 判別式$$D$$の値を計算する.
- $$D=B^{2}-4AC$$
- $$D\ge0$$のとき
- 赤$$RGB=(255,0,0)$$を出力
- それ以外のとき
- 青$$RGB=(0,0,255)$$を出力
0 Comments.