光の正体は電磁波であり, 電磁波は電界と磁界が進行方向に対して垂直に振動している波 (横波) です. 目に入ってくる自然光のほとんどはさまざまな方向に振動する光が混ざったものですが, 振動が一つの方向に偏っている光もあります. これを偏光といいます. さまざまな方向に振動している光から特定の方向にだけ振動している偏光を取り出すには偏光フィルタを使います. 偏光フィルタは特定の方向に振動する偏光以外を吸収するので, 自然光から偏光を作り出すことができます.
そこで, 偏光フィルタをもう一つ組み合わせます. 一つ目の偏光フィルタ (1) を通った偏光を, さらに二つ目の偏光フィルタ (2) に通します. このとき, 二つの偏光フィルタの方向が平行なら, 光は二つ目の偏光フィルタ (2) を通過します. しかし, 二つの変更フィルタの方向が直交していれば, 光は二つ目のフィルタ (2) でほとんど吸収されてしまいます (おまけ: 上図を描くプログラム).
なお, 偏光には上記のように振幅が特定方向に偏ったもの (直線偏光) の他に, 振幅の方向が光の進行方向に対して回転する円偏光や, 直線偏光と円偏光が組み合わさった楕円偏光もあります. 円偏光や楕円偏光には, 振幅の方向の回転方向によって, 右円偏光・右楕円偏光, 左円偏光・左楕円偏光があります.
実験に使用するディスプレイ (ZALMAN ZM-M220W, HYUNDAI W220S も同様) は, ディスプレイの液晶パネルに偏光フィルタが重ねられています. この偏光フィルタは, 液晶パネルの走査線ごとに, 右円偏光を通すものと左円偏光を通すものが交互に組み合わされています. これを右目と左目に右円偏光フィルタと左円偏光フィルタを取り付けた偏光メガネを通して見ると, 右目には奇数番目の走査線, 左目には偶数番目の走査線だけが見えます.
したがって, この裸眼 3D ディスプレイでは, 映像を次のように分けて画面表示する必要があります.
裸眼 3D ディスプレイを使用した立体視表示を行う場合, 図形を画面上の偶数番目の走査線と奇数番目の走査線に分けて描く必要があります. そのために, ここでは OpenGL の glPolygonStipple() という関数を使用します. この関数は図形を描く際に, 図形にかける「マスク」を定義します. マスクの大きさは 32×32 画素で, マスクの値が 1 のところだけ図形が描画されます. 従って次のようなマスクを用意すれば, 画面上の偶数番目の縦のラインや奇数番目の縦のラインだけに図形を描くことができます.
まず最初に, 初期化の関数 init() でこのマスクを作成します. また, GL_POLYGON_STIPPLE を有効にしておきます.
... #include <GL/glut.h> ... /* ** マスク用の変数の宣言 */ static int emask[32]; /* 偶数番目のライン用マスク */ static int omask[32]; /* 奇数番目のライン用マスク */ ... void init(void) { int i; /* 初期設定 */ ... /* マスクを作る */ for (i = 0; i < 32; ++i) emask[i] = ~(omask[i] = -(i & 1)); /* 描画するポリゴンにマスクをかける */ glEnable(GL_POLYGON_STIPPLE); }
どうしてこれで上のようなマスクが作成されるのかは, この処理を二進数で考えるとわかります. こうして奇数番目の走査線に右目からみたシーンを表示し, 偶数番目の走査線に左目から見たシーンを表示します.
また, この方法では同じシーンを 2 回描く必要があるので, 手間を省くためにディスプレイリストという機能を使用することにします. ディスプレイリストは, glNewList(番号, ...) 〜 glEndList() の間で実行した OpenGL の命令を, glCallList(番号)を呼び出すことによって再度実行するというものです. このディスプレイリストの番号は, glGenLists() を使って生成し, glDeleteLists() で破棄します.
/* * 画面表示 */ void display(void) { ... GLuint list; /* ディスプレイリスト */ /* ディスプレイリストの作成 */ list = glGenLists(1); /* 画面クリア */ glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); /* モデルビュー変換行列の初期化 */ glLoadIdentity(); /* 右目の位置と方向 */ gluLookAt(/* ここは自分で考えてください */); /* 視点の移動 */ ... /* 光源の位置を設定 */ ... /* 奇数番目の走査線だけに描画する */ glPolygonStipple((GLubyte *)omask); /* ディスプレイリストへの登録開始 */ glNewList(list, GL_COMPILE_AND_EXECUTE); /* シーンの描画 */ ... /* ディスプレイリストへの登録終了 */ glEndList(); /* デプスバッファだけをクリア */ glClear(GL_DEPTH_BUFFER_BIT); /* モデルビュー変換行列の初期化 */ glLoadIdentity(); /* 左目の位置と方向 */ gluLookAt(/* ここは自分で考えてください */); /* 視点の移動 */ ... /* 光源の位置を設定 */ ... /* 偶数番目の走査線だけに描画する */ glPolygonStipple((GLubyte *)emask); /* シーンの描画 */ glCallList(list); /* ディスプレイリストを破棄する */ glDeleteLists(list, 1); ... }
2つの視点の間隔とそれぞれの位置・方向, および fovy (実験1のサンプルでは 30 に設定されています) は, ディスプレイの表示面の高さ, 表示面との距離, 自分の両目の間隔の実測値から割り出してください.
本当は gluLookAt() を使わずに, gluPerspective() を glFrustum() に置き換えて左右の目の視野をずらしたほうが厳密なんですが, それだとプログラムが少しややこしくなりますし, glFrustum() の説明もしなきゃいけなくなるので手を抜きます.
最後に, 立体視表示しているウィンドウが小さかったり, 画面上に余計なものが映っていたりするとうまく立体感が得られない場合があるので, 画面をフルスクリーン表示にします.
... int main(int argc, char *argv[]) { glutInit(&argc, argv); glutInitDisplayMode(GLUT_RGBA | GLUT_DEPTH); glutCreateWindow(argv[0]); glutFullScreen(); glutDisplayFunc(display); ... }