WebGL Lesson 5 - Textureの導入

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

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


<< レッスン 4                                                                   レッスン 6>>

ようこそ私のWebGLチュートリアルシリーズのレッスン No.5へ。これはNeHe OpenGLチュートリアルのNo.6をもとにしています。
今回は3Dオブジェクトにテクスチャを追加します。 - これは別のファイルから読み込んだイメージで物体の表面を覆います。
これはオブジェクトを信じられないぐらい複雑に作り込むことなしに、3Dシーンにディテールを追加するのに非常に便利に使えます。
迷路タイプのゲームの石壁を想像して下さい。あなたはきっと壁にレンガのイメージを貼り付ける代わりに、
壁の中のそれぞれのブロックを別々のオブジェクトにするなんて事はしたくないですよね。

ここにWebGLをサポートしたブラウザで見るとどう見えるかがあります。

WebGL turorial, lesson 5 - introducing textures



WebGLに対応したブラウザを使っているなら、ここをクリックすれば生のWebGLバージョンが見れます。
もし対応したブラウザをもっていなければこちらから手に入れられます。

どうやって動いているか、詳細を見ていきましょう。

いつもの警告(英語):これらのレッスンはプログラミングの知識が十分にあって3Dグラフィックスの経験が無い人に向けて書かれています。
その目的は、あなたができる限り早く自身の3Dを使ったWebページを作れるように、コードの中で何が行われているかを理解してレベルアップして貰うことです。
もしあなたが以前のチュートリアルをまだ読んでいないなら、このレッスンを読む前に読んでおくべきです。
- ここではレッスン4からの差分と新しく現れるコードしか説明するつもりはありません。

チュートリアル中にバグや勘違いがあるかもしれません。もし何か問題を見つけたらコメントから教えて下さい。
できる限り早く修正します。

このサンプルのソースコードを手に入れる方法は2つあります。実際にサンプルが動いてるところから”ソースを表示”を選ぶか、
GitHubを使ってるならリンク先のリポジトリからCloneで取得できます。(その場合には先のレッスンも同時に取れます)
どちらの場合でも取得したらお好みのテキストエディタで開けばいいです。

テクスチャがどのように働くかを理解するコツは、3Dオブジェクト上の点の色を決める特別な方法だと認識することです。
レッスン2を思い出すと、色はfragment chaderによって設定されます、そのため、私たちがすべきことは、イメージを読み込み、fragment shaderに送ることです。
fragment shaderも処理しているfragmentに使用する画像の欠片はどれかを知る必要があり、そういった情報も送る必要があります。

それではtextureをロードするところから見ていきましょう。
その処理はページの下の方にあるwebGLStartからJavaScriptの処理が始まるとすぐに呼ばれます。(新しいコードは赤です)
  function webGLStart() {
var canvas = document.getElementById("lesson05-canvas");
initGL(canvas);
initShaders();
initTexture();

gl.clearColor(0.0, 0.0, 0.0, 1.0);
initTextureを見ていこう。 - だいたいファイルの上から1/3の位置にあります。全て新しいコードです。
  var neheTexture;
function initTexture() {
neheTexture = gl.createTexture();
neheTexture.image = new Image();
neheTexture.image.onload = function() {
handleLoadedTexture(neheTexture)
}

neheTexture.image.src = "nehe.gif";
}
textureを保持するglobal変数を作成します。
もちろん、実際には複数のtextureを利用するだろうし、global変数を使うべきではありません。
しかしここではシンプルにする方を優先します。
gl.createTextureを使ってtextureの参照を作成して先ほどのglobal変数に設定し、
次にtextureにattachするJavaScriptのImage Objectを作成して新しいattributeに保存します。
再びJavaScriptのどんなObjectにもどんなfiledでも設定出来るという長所を利用します。
texture objectにはデフォルトではimage fieldが有りませんが、これを追加しておくことは便利なので追加してしまいます。

次に明らかに行う必要があることはImage Objectに保存対象となる実際のイメージを読み込ませる事です。
それを行う前にコールバック関数をImage Objectに追加します。
この関数はイメージが完全に読み込み終わったときに呼び出されます。そのため先に登録しておくのが安全です。
登録が終わったらImage Objectのsrcプロパティを設定してやることはおしまいです。
イメージの読み込みは非同期に行われます。- そのためsrcプロパティへの設定は即座に完了します。
そして背後で動作するスレッドがweb serverからイメージを読み込みます。
読み込みが完了すれば登録したコールバック関数が呼ばれ、その中からhandleLoadedTextureが呼ばれます。
  function handleLoadedTexture(texture) {
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, texture.image);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.bindTexture(gl.TEXTURE_2D, null);
}
最初に行う事はWebGLにこのtextureが"current" textureであると伝えることです。
WebGLのtexture関数は引数として対象を指定するのではなく、全て"current" textureに対して行われます。
このbindTextureがcurrentを指定する関数です。
これは既に見てきたgl.bindBufferの時とそっくりですね。

次にWebGLにtextureにロードしたイメージを垂直方向に反転する必要があることを通知します。
これを行う理由は座標系に違いがあるためです。
今回扱うtextureの座標系は普段数学で使っているものと同じように、縦軸では上の方向が+になります。
これは頂点座標に利用しているX,Y,Zの座標系と一貫しています。
それとは対照的に、多くのグラフィックスシステム - たとえばtextureイメージに利用するGIFフォーマットでは縦軸の下方向が+となります。
横軸についてはどちらも同じ座標系です。

この縦軸での違いはWebGLの視点から、textureに利用するGIFイメージは既に縦軸方向で反転されているので"unflip"を指定する必要がある、ということを意味します。
(Ilmari Heikkinenのコメントのおかげで明確になりました)

次のステップは読み込まれたばかりのイメージをtexImage2Dを使ってグラフィクスカード上のtextureスペースへアップロードすることです。
引数を使って、どんな種類のイメージを利用するか、LOD(level of detail)(のちのレッスンで説明します)、
どのような形式でグラフィクスカードに保存するか(繰り返しになりますが、後のレッスンで説明します)、
イメージのそれぞれの"channel"のサイズ(赤、緑、青を保存するのに使うデータタイプ)、イメージ画像それ自身、を指定します。

続く2行のラインではtextureの特別なscalingパラメーターを設定します。
最初はWebGLにテクスチャがイメージのサイズに対して画面に大きく表示されるときに何を行うかを指定します。
言い換えると、どのように拡大するかのヒントを与えるということです。
次の行も同じようにヒントですが、こちらは縮小する場合です。
NEARESTはオリジナルのイメージをそのまま使わせるので選択肢の中でもっとも魅力に乏しいものです。
近くで見ると非常に汚らしく(blocky)見えるでしょう。
しかしこの方法には遅いマシン上でも高速に動作するという利点があります。
次回のレッスンでは別のスケーリングのヒントについて見ていきます。
そこでそれぞれのパフォーマンスと見た目の品質について比較できるでしょう。

ここまで完了したらcurrent textureにnullを設定しています。これは必ずしも必要な事ではありません。
しかし良い習慣です;使った後をきちんと片付けるという意味で。

これでtextureの読み込みに関係するコードは全ておしまいです。次はinitBuffersを見てみましょう。
もちろんlesson 4で存在したピラミッドに関する部分が全て取り除かれています。
しかしもっと興味深いのはキューブのvertex colour bufferが新しいものに置き換わっている事です -
texture coordinate buffer。こんな感じです。
    cubeVertexTextureCoordBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, cubeVertexTextureCoordBuffer);
var textureCoords = [
// Front face
0.0, 0.0,
1.0, 0.0,
1.0, 1.0,
0.0, 1.0,

// Back face
1.0, 0.0,
1.0, 1.0,
0.0, 1.0,
0.0, 0.0,

// Top face
0.0, 1.0,
0.0, 0.0,
1.0, 0.0,
1.0, 1.0,

// Bottom face
1.0, 1.0,
0.0, 1.0,
0.0, 0.0,
1.0, 0.0,

// Right face
1.0, 0.0,
1.0, 1.0,
0.0, 1.0,
0.0, 0.0,

// Left face
0.0, 0.0,
1.0, 0.0,
1.0, 1.0,
0.0, 1.0,
];
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(textureCoords), gl.STATIC_DRAW);
cubeVertexTextureCoordBuffer.itemSize = 2;
cubeVertexTextureCoordBuffer.numItems = 24;
もうこの手の処理は安心して読めるでしょう。
そしてここでやっていることは頂点毎のattributeをbufferの中に設定しているという事もわかるでしょう。頂点毎に2つの値を持つと言うことも。
これらのtexture座標が意味するのはデカルト座標(x,y)でtexture上のどこに頂点が位置するかです。
Textureの大きさは正規化されるので大きさは高さ1、幅1となり、(0,0)が左下、(1,1)が右上に対応します。

initBuffersの変更はこれだけです。なのでdrawSceneへ移りましょう。
この関数のもっとも面白い変更はもちろんtextureを使うようにしている部分です。
しかしながら、その部分に目を通す前にプラミッドの削除とそれによるキューブの回転方法の変更に関するいくつかの単純な変更を見ていきましょう。
私はその部分の詳細について、非常に簡単なので解説するつもりはありません。
それらは、このdrawScene関数の先頭部分の中で赤字で強調されています。

  var xRot = 0;
var yRot = 0;
var zRot = 0;

function drawScene() {
gl.viewport(0, 0, gl.viewportWidth, gl.viewportHeight);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

perspective(45, gl.viewportWidth / gl.viewportHeight, 0.1, 100.0);
loadIdentity();

mvTranslate([0.0, 0.0, -5.0])

mvRotate(xRot, [1, 0, 0]);
mvRotate(yRot, [0, 1, 0]);
mvRotate(zRot, [0, 0, 1]);


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

animete関数にもxRot,yRot,zRotを更新する処理に対応する変更がありますが、説明しません。

本筋以外の説明が終わったので、textureに関する部分をみていきましょう。
initBuffers関数でtexture座標を含むbufferをセットアップしたのでshaderからそれらを利用できるように、適切なattributeにbindする必要があります。

・・・これでWebGLに拡張点がtextureのどの部分を使うかを伝えました。
先ほど読み込んだtextureを利用する事を伝えて、キューブを描画します。

    gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, neheTexture);
gl.uniform1i(shaderProgram.samplerUniform, 0);


gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, cubeVertexIndexBuffer);
setMatrixUniforms();
gl.drawElements(gl.TRIANGLES, cubeVertexIndexBuffer.numItems, gl.UNSIGNED_SHORT, 0);

ここで起こっている事は少し複雑です。WebGLではgl.DrawElements等の関数の呼び出しに対して最大で32枚までのtextureを利用でき、
それらはTEXTURE0からTEXTURE31という番号付けがされています。
ここでやっていることは最初の二行ではtexture 0に先ほどロードしたtextureを使う事を宣言しており、三行目でshaderのuniform変数に対して0という番号を渡しています。
(uniform変数については他のmatrixのために利用する場合と同じようにinitShaderの中でshaderから取得してあります);ここではtexture 0を使うとshaderに伝えています。
どのようにこの値が使われるかは後で見ていきます。

ともかく、この三行が実行されると先に進む準備ができます。
そこで、キューブを形作る三角形を描画する、以前と同じコードを実行します。

残っている説明が必要な新しいコードはshaderだけです。最初にvertex shaderを見ていきましょう。

  attribute vec3 aVertexPosition;
attribute vec2 aTextureCoord;

uniform mat4 uMVMatrix;
uniform mat4 uPMatrix;

varying vec2 vTextureCoord;

void main(void) {
gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0);
vTextureCoord = aTextureCoord;
}

これはレッスン2で色に関連する処理を行ったvertex shaderに非常に似ています。
ここで行われていることは色の代わりにtexture座標を頂点毎のattributeから受け取ってvarying変数に出力しているだけです。

全てのvertexに対して処理が呼ばれると、WebGLはfragment(基本的に全てのpixel毎にでしたよね)のために頂点間の出力された値を線形補完します。
- レッスン2で色を扱ったように。
そのため、(1,0)のtexture座標と(0,0)のtexture座標を持った頂点の真ん中のfragmentではtexture座標は(0.5,0)が得られるでしょう。
(0,0)と(1,1)の間では(0.5,0.5)が得られるでしょう。
次はfragment shaderです。
  #ifdef GL_ES
precision highp float;
#endif

varying vec2 vTextureCoord;

uniform sampler2D uSampler;


void main(void) {
gl_FragColor = texture2D(uSampler, vec2(vTextureCoord.s, vTextureCoord.t));
}
ここで補完されたtexture座標を取り出します。またshaderでtextureを表すsamplerタイプの値も持っています。
drawSceneでtextureはgl.TEXTURE0にbindされ、uniform変数のuSamplerには0がセットされています。
そのたもこのsamplerは私たちのtextureを表しています。
shaderが行う事はtexture2D関数を使って適切な色をtexture座標を使ってtextureから取り出すことです。

fragmentのための色が出来たら完了です。
スクリーンにはtextureの張られたobjectが現れています。

はい。これで今回はおしまいです。これで今回のレッスンで学ぶことは全てわかりましたね。
WebGLで3D objectにtextureを張る方法はイメージを読み込んで、WebGLにtextureを使う事を通知してobjectにtexture座標を与えて、
shaderの中でtexture座標を使えばいいのです。

もし質問やコメント、間違いの指摘など有りましたら、下のコメント欄に残して下さい。

特になければ次のレッスンを見て下さい。次回はWebページを見ている人から操作できるように、JavaScriptでキーの取得を行って3Dシーンを動かす方法を解説します。
それを使って見ている人にキューブの回転やズームイン、ズームアウトなどを行えるようにします。
またWebGLにtextureのスケーリングのためにWebGLに与えるヒントを調整します。

<< レッスン 4 レッスン 6>>

謝意:Chris MarrinのWebKit-onl spinning boxはこの記事を書くときにとても助かりました。Jacob SeidelinのFirefox用のport of Chris' demoと同じように。
毎回の事ですが、NeHeOpenGLチュートリアルにこのレッスンのためのスクリプトを書くのに大変助けられています。
Comments