WebGL Lesson 3 - 動き

オリジナル(英語)のLesson 3はこちら

もし翻訳後の文章に間違いや気になる点など有りましたら、お気軽にこのページの下のコメント欄に書き込んでください。
できる限り対応します。

<< レッスン 2                                                                                                                                   レッスン 4>>

WebGLチュートリアル3回目にようこそ!今回は動きをつけていきます。
今回のチュートリアルはNeHeOpenGLチュートリアル4回目を参考にしています。

今回のレッスンの実行結果です。

もしあなたの使用しているブラウザがWebGLをサポートしているなら(入手方法はここです)、ここで実際に動作するデモを見ることができます。

詳しい動作内容は以下で説明します。

いつもの注意書き:これらのレッスンはプログラミングの知識はあるが、3Dグラフィックスの経験がない人を対象にしています。
つまり、コードの中で何が起こっているかをよく理解し、実際に動かすことを目的としています。
なので、あなたは3Dのウェブページをすぐにつくり始めることができます。
今回のレッスンはレッスン2との差分しか説明されていないので、まだレッスン1,2を読んでいないのならばそちらを先に読むことをお勧めします。

このチュートリアルには、バグやおかしいところがあるかもしれません。
もし見つけたら、私にコメント欄でコメントしてください。できるだけすぐに直します。

このサンプルコードを入手する方法は二つあります。ひとつはデモページのソースコードを開くこと。
もうひとつはもしGitHubを使えれば、レポジトリからcloneコマンドを実行することです。
どちらにせよ、コードを入手したら一度は好きなエディタで開いて中身を見てみてください。

コードの中身を説明する前に、ひとつはっきりさせておきたいことがあります。
WebGLにおいて、3Dシーンをアニメーションさせることはとてもシンプル-つまり、異なるシーンを連続して描画するだけです。
これは多くの読者にとって言うまでもないことかもしれませんが、私がWebGLを勉強し始めた頃は少し驚いたことなので、初めてWebGLを勉強する人は驚くかもしれません。
私が混乱した理由は、もっと高級、抽象化された手順で実行するものと考えていたからです。
例えば、ポイントXに描画された四角形があります。その四角形を動かすためには3DシステムにポイントYへ動かせ、と命令すると自動的にアニメーションが実現されるものだと考えていました。
しかし実際はそうではなく、次はポイントYに描画、次はポイントZに描画、と連続して命令していき、描画ごとに描画位置を変更することによってアニメーションを実現させます。

最後のパラグラフが少なくとも誰かの理解の役に立てば幸いです(もし混乱のもととなるようでしたらコメント欄で知らせてください。削除します。:-)

ともかく、今までのサンプルでは全てを描画するのにdrawSceneという関数を次の用に利用してきた事からも、この事を示しています。
   setInterval(drawScene, 15);

・・・JavaScriptに対して15ms間隔毎にdrawSceneを呼び出すように伝える。
シーンをアニメーションさせて三角形と四角形を動かすために必要な事は、drawSceneが呼ばれる毎に少しずつ異なるものを描くようにコードを修正する事だけです。

これはレッスン2からの変更点はdrawSceneにあるということです。
なので、そこから見ていきましょう。index.htmlを3分の2程スクロールダウンしてください。まず最初に目に入るのは、関数の前に新しく定義された2つのグローバル変数です。
var rTri = 0; var rSquare = 0;
これらは、それぞれ三角形と四角形の回転を保存しておくものです。両方共0度から始まり、毎時間ごと少しずつ回転できるように(あとでわかります)増えていきます。(余談ですが、今回のデモのようにグローバル変数を使用することは、実際には良くないことです。レッスン9においてもっとエレガントな構造を示します)
次のdrawSceneにおける変更点は、三角形を描画する部分です。
その部分のコードを全て載せますが、新しい部分は赤く表示されています。
    perspective(45, gl.viewportWidth / gl.viewportHeight, 0.1, 100.0);
    loadIdentity();

    mvTranslate([-1.5, 0.0, -7.0])

    mvPushMatrix();
    mvRotate(rTri, [0, 1, 0]);

    gl.bindBuffer(gl.ARRAY_BUFFER, triangleVertexPositionBuffer);
    gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, triangleVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);

    gl.bindBuffer(gl.ARRAY_BUFFER, triangleVertexColorBuffer);
    gl.vertexAttribPointer(shaderProgram.vertexColorAttribute, triangleVertexColorBuffer.itemSize, gl.FLOAT, false, 0, 0);

    setMatrixUniforms();
    gl.drawArrays(gl.TRIANGLES, 0, triangleVertexPositionBuffer.numItems);

    mvPopMatrix();
ここで何が起きているか説明するためにレッスン1へ戻ってみましょう。私はこう言いました。
OpenGLではシーンを描画するときに、対象の物体毎に、"current"の位置と回転を伝えます。 - 
たとえば、「前に20進んで32°回転してロボットを描画して」という具合に。
最後の複雑な部分は「どれだけ進んで、どれだけ回転して、それから描画して」という指示の中にあります。
これは”ロボットを描画する”コードを1つの関数にカプセル化できるのでとても便利です。
そして関数を呼ぶ前に移動や回転を変えることで簡単にロボットを移動させることができます。
現在の状態はmodel-view配列に格納されることを覚えておいてください。
ここまでの情報が与えられれば、次のコード:
    mvRotate(rTri, [0, 1, 0]);

の意味は明らかですね。;

model-view matrixに保存されるcurrentの回転量を垂直な軸の周りをrTriで示す角度だけ回転させることで変更します。(軸は2番目に渡す引数で配列として指定します)
これは三角形が描画されたときに、rTri度分だけ回転していると言うことです。
mvRotateはlesson 1で見たmvTranslate関数と同じようにJavaScriptで書かれています。 - mvRotete関数は後で見ます。

さて、mvPushMatrixとmvPopMatrixとはなんでしょうか?
名前から予想が付くかもしれませんが、これらもmodel-view matrixに関連しています。
ロボットを描画するという私のたとえ話に戻り、ポイントAに移動する、ロボットを描画する、ポイントAからオフセット分移動する、ティーポットを描画するといった粒度の抽象レベルでコードを見てください。
ロボットを描画するコードでは様々なmodel-view matrixの変更が行われます。
それはロボットの胴体を配置して、脚を下につけ、頭を上にのせ、最後に腕をつけます。
問題はこのようにした後、ポイントAからオフセット分移動させようとしたときに起きます。
ポイントAではなく、最後に描画したものに対してオフセット分移動するので、ロボットの腕をあげると、ティーポットは浮いた状態で描画されます。
これは意図した通りではありませんよね。

要求される明らかなことはロボットを描画する前にmodel-view matrixを保存する方法と、後で元に戻す方法ですね。
もちろんこれはmvPushMatrixとmvPopMatrixが行います。
mvPushMatrixはmatrixをstackに積みます。mvPopMatrixはstackのトップからmatrixを取り出してcurrentのmatrixを置き換えます。
Stackを利用すると言うことは、何段階でも入れ子になった描画コードを利用できると言うことです。
それぞれの描画コードの中ではmodel-view matrixを操作して描画後には元に戻します。
そのため、回転した三角形の描画が完了したらmvPopMatrixを使ってmodel-view matrixを戻します。
その目的は次のコード:
mvTranslate([3.0, 0.0, 0.0])
・・が回転していない基準の座標系でシーンを横切るように(model-view matrixを)移動させるようにです。
(もしまだこれらの意味がよく分かっていないなら、コードをコピーしてpush/popを削除した後に実行して何が起こるかを観察することをお勧めします。;見ればすぐに確実にぴんと来るでしょう。 )
これらの3つの変更で三角形が四角形に影響を与えることなく中心を通る垂直な軸の周でを回転できるようになります。
また四角形が中心を通る水平な軸の周りを回転するための、同じような3行の処理があります。
    mvPushMatrix();
    mvRotate(rSquare, [1, 0, 0]);

    gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexPositionBuffer);
    gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, squareVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);

    gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexColorBuffer);
    gl.vertexAttribPointer(shaderProgram.vertexColorAttribute, squareVertexColorBuffer.itemSize, gl.FLOAT, false, 0, 0);

    setMatrixUniforms();
    gl.drawArrays(gl.TRIANGLE_STRIP, 0, squareVertexPositionBuffer.numItems);

    mvPopMatrix();
  }

・・これがdrawSceneの中の描画のためのコードの変更の全てです。

シーンをアニメーションさせるために明らかに必要な他の事はそれぞれの描画で少しずつ異なるシーンが描画されるように、rTri とrSquareを時間の経過に合わせて変更することです。
この処理をdrawSceneと同じように定期的に呼び出されるanimateという新しい関数の中で行います。
(その処理を行うコードは一瞬で理解出来るでしょう)
それはこのような処理です。:
var lastTime = 0; function animate() { var timeNow = new Date().getTime(); if (lastTime != 0) { var elapsed = timeNow - lastTime; rTri += (90 * elapsed) / 1000.0; rSquare += (75 * elapsed) / 1000.0; } lastTime = timeNow; }
シーンをアニメーションさせる簡単な方法は、animate関数が呼ばれるたびに一定の値を単純に足すことでしょう。
(このレッスンのベースにしているオリジナルのOpenGLのレッスンでもそのように行っています)
しかしここでは、私がもう少し良い方法と考えている手法を選択します。
;オブジェクトの回転する量は、前回関数を呼んでからどれだけ経過したかによって決定されます。
もっと正確に言えば、三角形は1秒間に90度、四角形は1秒間に75度回転します。
この方法の良いところは、どんなに速いマシンを使っているかに関わらず、誰でも同じ速さで動くシーンが見られる事です。
遅いマシンを使っている人は、単純に動きが荒くなるだけでしょう。
この事は今回のようなシンプルなデモでは得に問題になりませんが、ゲーム等の場合には明らかに問題となります。

次の変更はanimate関数をdrawSceneと同じように定期的に呼ばれるようにする部分です。
これをtickと呼ばれる内部で新しい関数を作って、この中でdrawScenとanimateを呼ぶようにして、drawSceneの代わりに15ms毎に呼ばれるようにします。
  function tick() {
    drawScene();
    animate();
  }

  function webGLStart() {
    var canvas = document.getElementById("lesson03-canvas");
    initGL(canvas);
    initShaders();
    initTexture();

    gl.clearColor(0.0, 0.0, 0.0, 1.0);
    gl.clearDepth(1.0);

    gl.enable(gl.DEPTH_TEST);
    gl.depthFunc(gl.LEQUAL);

    setInterval(tick, 15);
  }
これがシーンのアニメーションと描画に関するコードの変更の全てです。
それでは追加が必要だったサポート処理のコードを見ていきましょう。
最初はmvPushMatrixとmvPopMatrixです。:
var mvMatrixStack = []; function mvPushMatrix(m) { if (m) { mvMatrixStack.push(m.dup()); mvMatrix = m.dup(); } else { mvMatrixStack.push(mvMatrix.dup()); } } function mvPopMatrix() { if (mvMatrixStack.length == 0) { throw "Invalid popMatrix!"; } mvMatrix = mvMatrixStack.pop(); return mvMatrix; }
そこには驚くようなものは何もないですね。
Matrixを保持するためのlistを持って適切にpushとpopを定義するだけです。

次はmvRotateを見てみましょう。
function mvRotate(ang, v) { var arad = ang * Math.PI / 180.0; var m = Matrix.Rotation(arad, $V([v[0], v[1], v[2]])).ensure4x4(); multMatrix(m); }
これもかなりシンプルですね。 - 回転を表すmatrixを作成するハードな仕事はSylvesterのlibraryで行えます。

そして・・これでお仕舞いです!
これ以上の変更はありません。
これであなたはWebGLの簡単なシーンをアニメーションさせる方法を知りました。
もし何かコメントや修正があれば下にコメントを残して下さい。

次回は(NeHeの序文を彼のLesson 5に引用するために)2Dオブジェクトを3Dワールドに出すのではなく、オブジェクトを本物の3Dオブジェクトとして作成します。
方法を知るためにここをクリックして下さい。

謝辞:mvPushMatrix,mvPopMatrix,mvRotateのコードはVladimir Vukićevićのspore crature viewerからのいただきました。
またNeheOpenGL チュートリアルにこのレッスンのスクリプトを作るために深い恩義を感じています。

Comments