Development

ITpro  > Development
Development

Java技術最前線

ITpro

「OpenGLを使ってJavaでも3Dを楽しもう」
第6回 光と素材

設計図
図1 ロボットの設計図

今週でやっと今までの線画とさようならです。というのも,3Dで描画するモデルを照らす光と,モデルの素材を設定するからです。

でも,せっかく線画でなくなるのに,ただの立方体じゃ悲しいですね。ということで,まずモデルをちょっとだけ豪華にしてみましょう。

今週,完成予定のサンプルはここからダウンロードできます Robot.java

ロボット?

何にするかというとロボットです。といっても,図1に示したように,球と直方体だけでできている,とても単純なロボットです。

さて,これをどのように作っていきましょうか。実をいうと今まで解説したことだけで,実現できるのです。

順々にいってみましょう。まずは胴体からです。

胴体は立方体をベースに作成します。立方体のままだと太っているので,少しスリムにしましょう。単に立方体を表示するには次のようなコードになります。

    glut.glutSolidCube(1.0f);

ここで,幅と高さは変えずに奥行きだけ減らします。つまり,z軸方向だけを縮小するのです。

    gl.glPushMatrix();
    gl.glScalef(1.0f, 1.0f, 0.5f);
    glut.glutSolidCube(1.0f);
    gl.glPopMatrix();
胴体
図2 胴体

拡大・縮小などの変換を行うときにはGL#glPushMatrixメソッドをコールして,行列を保存してから行います。その後,GL#glScalefメソッドで拡大・縮小します。

これで図2のような直方体が表示されます(図2はわかりやすいように回転してあります)。

次に腕を描画してみましょう。

腕も胴体と同様に立方体を拡大・縮小して作ります。腕は胴体よりも細いので,幅と奥行きを0.2,長さを1.2にしてみました。

    gl.glPushMatrix();
    gl.glScalef(0.2f, 1.2f, 0.2f);
    glut.glutSolidCube(1.0f);
    gl.glPopMatrix();

腕だけを表示させたのが,図3です。

ここまではうまくいきました。問題は胴体と腕を両方とも描画しなくてはならないということです。単純に二つのコードを組み合わせてみたのが,図4です。胴体も腕も原点を中心に描画されるので,重なってしまいました。

これでは何がなんだかさっぱりわかりません。

腕 胴体と腕
図3 腕 図4 胴体と腕

そこで,腕を移動させます。まず,x軸方向に動かしましょう。胴体の幅が1で,腕の幅が0.2なので,胴体の横に腕がくるようにするには(1+0.2)/2=0.6移動させればいいですね。

ただし,0.6移動させると胴体と腕がピッタリくっついてしまうので,少しだけ間を開けさせるために,0.65移動させます。

y軸方向は腕の上端と胴体の上端が同じになるようにしましょう。原点を中心に描画した場合,腕の上端のy座標は0.6,胴体は0.5になっています。そこで,腕を-0.1移動させます。

    // 胴体
    gl.glPushMatrix();
    gl.glScalef(1.0f, 1.0f, 0.5f);
    glut.glutSolidCube(1.0f);
    gl.glPopMatrix();
 
    // 左手
    gl.glPushMatrix();
    gl.glTranslatef(0.65f, -0.1f, 0.0f);
    gl.glScalef(0.2f, 1.2f, 0.2f);
    glut.glutSolidCube(1.0f);
    gl.glPopMatrix();

これを表示させたものが図5です。ほら,何となく胴体と腕に見えてきませんか?

ここまでやれば後は同じ。左手のx軸方向の移動が0.65ですから,右手は-0.65にすればOKです。

足も同じように立方体を拡大・縮小し,移動させて作ります。

胴体と腕 胴体と手足
図5 胴体と腕(移動後) 図6 胴体と手足

最後に残ったのが頭です。

頭も直方体で作ってもいいのですが,全部同じなのも味気ないですね。ということで,球で作ってみました。

球も立方体と同様にGLUTクラスを使用して作成します。使用するメソッドはglutSolidSphereメソッドです。

このメソッドで描画する球は厳密な意味では球ではありません。というのも,球を緯線と経線で区切った三角形もしくは四角形で球を近似させているからです。どの程度,細かく切り刻むかは引数で指定します。

glutSolidSphereの第1引数は半径,第2引数が経線の分割数,第3引数が緯線の分割数になります。これらの数字が大きくなればなるほど球に近づきます。ここでは,20ずつで実行させました。

ちょっと長くなりますが,ロボットを描画する部分全体を示しておきます。

    // 胴体
    gl.glPushMatrix();
    gl.glScalef(1.0f, 1.0f, 0.5f);
    glut.glutSolidCube(1.0f);
    gl.glPopMatrix();
 
    // 左手
    gl.glPushMatrix();
    gl.glTranslatef(0.65f, -0.1f, 0.0f);
    gl.glScalef(0.2f, 1.2f, 0.2f);
    glut.glutSolidCube(1.0f);
    gl.glPopMatrix();
 
    // 右手
    gl.glPushMatrix();
    gl.glTranslatef(-0.65f, -0.1f, 0.0f);
    gl.glScalef(0.2f, 1.2f, 0.2f);
    glut.glutSolidCube(1.0f);
    gl.glPopMatrix();
 
    // 左足
    gl.glPushMatrix();
    gl.glTranslatef(0.3f, -1.05f, 0.0f);
    gl.glScalef(0.2f, 1.0f, 0.2f);
    glut.glutSolidCube(1.0f);
    gl.glPopMatrix();
 
    // 右足
    gl.glPushMatrix();
    gl.glTranslatef(-0.3f, -1.05f, 0.0f);
    gl.glScalef(0.2f, 1.0f, 0.2f);
    glut.glutSolidCube(1.0f);
    gl.glPopMatrix();
 
    // 頭
    gl.glPushMatrix();
    gl.glTranslatef(0.0f, 0.95f, 0.0f);
    glut.glutSolidSphere(0.4f, 20, 20);
    gl.glPopMatrix();

ロボット
図7 ロボット

これで,ロボットのできあがりです(図7)。何が何でもロボットなんです。誰がなんといおうとロボットです(キッパリ)。

このように基本的な立体を変形させながら組み合わせることで,様々な形状を表すことが可能です。

とはいうものの,上記のコードには同じような処理が並んでいるので,もう少しリファクタリングする必要があります。

ここでは見やすさのために,数字もわざとそのまま残してありますが,これらも定数化する必要がありますね。

3DのCGを表記するときには,ここで示したようにどうしても同じような処理がずらずらと並んでしまいがちです。なるべく構造化し,見通しのいいコードを書くようにしましょう。

前置きがすっかり長くなってしまいました。これで,やっと光と素材についての話に移ることができます。

光と素材

3Dにはあって,2DのCGでは登場してこない要素に光があります。実際には2DのCGでも光を扱っているのですが,ほとんど意識したことがないはずです。

例えば,図7のウィンドウの枠をご覧ください。左上が明るい青で,右下が暗い青に色が変化しています。これは暗黙的に左上に光があるということを意味しています。

2Dではこのように暗黙的にしか認識されていませんが,3Dは違います。どこから光が当たるかによって,モデルの見え方がまるで変ってくるのです。明るい部分と,暗い部分があってこそ,モデルの形状を認識できます。

そこで,まず3Dにおける光について説明しましょう。

ところで,ここまで単に光と呼んでいましたが,実際には光だけではだめで,物体による反射も考慮する必要があります。OpenGLで扱う光には次の4種類あります。

鏡面反射 光の入射角度により,反射角度が一意的に決まる反射光
ガラスや金属など
拡散反射 入射角度によらず,反射があらゆる方向に散乱する反射光
プラスティックなど
環境光反射 いわゆる地あかり
方向をもたず全体的に照らす環境光に対する反射光
放射 物体から放射される光

放射は陰影処理には使用されないので,主に鏡面反射,拡散反射,環境光反射の3種類を扱っていきます。

これらの反射をコードに記述する前に,次のようなコードを記述する必要があります。

    gl.glEnable(GL.GL_LIGHTING);
    gl.glEnable(GL.GL_LIGHT0);
光を当てたロボット
図8 光を当てたロボット

1行目が光を使用可能にするための設定,2行目が0番目の光を使用可能にする設定です。この記述を見ればおわかりだと思いますが,GL_LIGHT0の0が光の番号になっています。例えばGL_LIGHT2のように数字の部分を変えることで,複数の光を設定できます。

さて,この2行をRobotクラスのinitメソッドに記述して,実行してみました(図8)。上記の2行だけでは光の位置や強さなどがデフォルトの値になります。

図8を見ると,ちゃんと光が当たっている様子がわかります。しかし,なんか変ではないですか。

明暗差が妙に大きくなっているような感じです。

ここでは大きな要素が抜けているのです。学校で習った反射のことを思い出してください。鏡面反射の場合,反射する光は物体の表面から垂直な線(法線)に対して,角度が同じになりますよね。

3Dでもこれは同じです。法線は3Dなので,法線ベクトルと名前は変わりますが,法線ベクトルと入射光ベクトルさえわかれば,反射光ベクトルを特定できます。

法線と反射
図9 法線と反射

ところが,問題なのはこの法線ベクトルを各面に対して設定してしなくてはならないということです。しかし,すべてを設定するのは大変ですよね。なんと,OpenGLでは自動的に法線を計算してくれるようにできるのです。このためには次のようなコードを記述して,法線ベクトルの長さを1に正規化する必要があります。

    gl.glEnable(GL.GL_NORMALIZE);
光を当てたロボット
図10 法線の正規化を行った場合

さて,これで実行してみましょう。

図8と比べると光の当たり方がずいぶん柔らかくなった感じですね。

さて,ここまではデフォルトの光の位置と反射を使用してきました。それをプログラムの中で設定してみましょう。

光の設定を行うにはGLクラスのglLightfv/glLightivメソッドを使用します。fはfloat,iはintをあらわしています。最後のvはベクトル,つまり配列もしくはBufferを示しています。

設定できるのは光の位置(GL_POSITION),鏡面(GL_SPECULAR),拡散(GL_DIFFUSE),環境光(GL_AMBIENT)です。

GL_POSITIONはx,y,z座標で表しますが,指定する配列は要素数4のものを指定します。4番目の要素は光源までの距離を表すファクタで,通常は無限遠を表す0を指定します。

その他のGL_SPECULARなどは色を表すR,G,B,アルファを指定します。glLightfvメソッドを使用するときには,それぞれの色要素は0から1までの値となります。

例えば,次のように記述してみましょう。

    float[] position = { -10.0f, 10.0f, 10.0f, 0.0f };
    float[] specular = { 1.0f, 0.5f, 0.5f, 1.0f };
    float[] diffuse = { 1.0f, 0.5f, 0.5f, 1.0f };
    float[] ambient = { 0.8f, 0.8f, 0.8f, 1.0f };
 
    gl.glLightfv(GL.GL_LIGHT0, GL.GL_POSITION, position, 0);
    gl.glLightfv(GL.GL_LIGHT0, GL.GL_SPECULAR, specular, 0);
    gl.glLightfv(GL.GL_LIGHT0, GL.GL_DIFFUSE, diffuse, 0);
    gl.glLightfv(GL.GL_LIGHT0, GL.GL_AMBIENT, ambient, 0);
赤いスポットライトを浴びたロボット
図11 赤いスポットライトを浴びたロボット

glLightfvメソッドの第1引数は光の番号,第2引数が設定する要素で,第3引数がその値になります。第4引数はオフセット,つまり第3引数で指定する配列の読み始めのインデックスを表しています。

光は(-10, 10, 10)の方向にあるので,左上の手前側から光が当たるようになっています。

GL_SPECULARとGL_DIFFUSEはR=1.0,G=0.5,B=0.5なので,赤っぽい光です。これに対して環境光(GL_AMBIENT)はR=0.8,G=0.8,B=0.8ですから,灰色になります。

これはどういうことかというと,左上手前からの赤いスポット光が当たり,その光が届かないところは灰色になるということを表しています。

図11を見るとわかるのですが,頭が一番わかりやすいですね。左上の部分は赤く照らされていますが,裏側の部分は灰色になっています。

これで光は設定できました。

ところがこれで全部ではないのです。モデル側の設定もしなくてはなりません。

モデル側で設定するのは,光に対する反射率です。鏡面反射,拡散反射,環境光反射のそれぞれの色要素の反射率を設定します。

このとき,使用するのがGL#glMaterialiv/glMaterialfvメソッドです。

また,光が当たった部分のハイライトの大きさを表す光輝パラメータも指定します。この値が大きいほど,輝く部分が小さくなり,結果的にモデルが輝いて見えます。

光輝パラメータは普通の値なので,GL#glMaterialfメソッドを使用します。

例えば,次のように記述してみましょう。光の設定は先ほどと同じです。

    float[] specular = { 0.5f, 0.5f, 0.8f, 1.0f };
    float[] diffuse = { 0.5f, 0.5f, 0.8f, 1.0f };
    float[] ambient = { 0.2f, 0.2f, 0.4f, 1.0f };
    float shininess = 10.0f;
  
    gl.glMaterialfv(GL.GL_FRONT, GL.GL_SPECULAR, specular, 0);
    gl.glMaterialfv(GL.GL_FRONT, GL.GL_DIFFUSE, diffuse, 0);
    gl.glMaterialfv(GL.GL_FRONT, GL.GL_AMBIENT, ambient, 0);
    gl.glMaterialf(GL.GL_FRONT, GL.GL_SHININESS, shininess);
青いロボットに赤いスポットライト
図12 青いロボットに赤いスポットライト

GL_SPECULARなどはすべてBが高い値になっています。つまり,青い物体であることを示しています。

これに対して,光は赤なので,全体的には光が当たっている部分は紫になっています。

このように光と素材のそれぞれに対して設定を行うことで,画面上に描画される色が決定されるのです。

今週は3Dのモデルとしてロボットを作成しました。そして,ロボットを題材にして,光や素材について説明を加えました。

来週はこのロボットを動かしてみましょう。

著者紹介 櫻庭祐一

横河電機の研究部門に勤務。同氏のJavaプログラマ向け情報ページ「Java in the Box」はあまりに有名

(ITpro)  [2006/08/14]

この記事に対する読者コメント

コメントに関する諸注意 コメント投稿 コメント一覧