WebGL (7)


テクスチャ

物体表面にテクスチャを貼る手順について説明します。

  1. テクスチャとして用いる画像を用意する。
  2. javascriptから頂点シェーダに各頂点のテクスチャ座標系での位置を渡す。
  3. javascriptでテクスチャ画像を読み込む。
  4. WebGLでテクスチャ画像を使用できるようにする。
  5. フラグメントシェーダに、利用するテクスチャユニットの番号を渡す
  6. 頂点シェーダで、テクスチャ座標をフラグメントシェーダに転送する。
  7. フラグメントシェーダで、テクセルの色を取り出しフラグメントに設定する。

テクスチャ上の位置は テクスチャ座標 (s,t)で表し、元の画像の大きさとは関係なく 0 ≤ s ≤ 1, 0 ≤ t ≤ 1 です。 テクスチャ座標系と、画像イメージの座標系ではy軸が反転していることに注意が必要です。

   t ^
(0,1)|
     |
     |
(0,0)+--------> s
           (1,0)
           (1,0)
(0,0)+--------> x
     |
     |
     |
(0,1)v
テクスチャ座標系画像データの座標系

1. テクスチャとして用いる画像を用意する

WebGL や OpenGL/ES でテクスチャとして使用できる画像には次のような制約があります。

下のプログラムで利用しているのは1024x1024のjpegファイルです。

sakura1024.jpg

2. javascriptから頂点シェーダに各頂点のテクスチャ座標系での位置を渡す

一つのポリゴンを構成する各頂点に対して、その頂点がテクスチャ座標系で どこにあるかを与えておきます。この値をattribute変数で頂点シェーダに渡し、 頂点シェーダからvarying変数を経由してフラグメントシェーダに 渡すことで、各フラグメントごとのテクスチャ座標が補間によって求められて フラグメントシェーダに渡されます。

//    v6----- v5
//   /|      /|
//  v1------v0|
//  | |     | |
//  | |v7---|-|v4
//  |/      |/
//  v2------v3
tsuda.cube = {};
tsuda.cube.vertices = new Float32Array([
  // x, y, z
  1.0,  1.0,  1.0,  // 0 v0 :front  
 -1.0,  1.0,  1.0,  // 1 v1	      
 -1.0, -1.0,  1.0,  // 2 v2	      
  1.0, -1.0,  1.0,  // 3 v3	      

...(略)

tsuda.cube.texCoord = new Float32Array([
  1.0, 1.0,  // v0 :front
  0.0, 1.0,  // v1
  0.0, 0.0,  // v2
  1.0, 0.0,  // v3

...(略)

3. javascriptでテクスチャ画像を読み込む

WebGLではテクスチャ用の画像はネットワーク経由でブラウザに非同期的に読み込まれます。 画像データにアクセスできるのは読み込みが全て終了した後である点に注意しましょう。

画像の読み込みと、その後の処理はjavascriptで以下のように記述します。

  1. Imageオブジェクト(画像オブジェクト)を作成する。
  2. var image = new Image();

  3. 画像を読み終えたときに呼び出されるイベントハンドラを設定する。
  4. image.onload = function() { 画像が読み込まれた後で実行すべき関数呼び出し; };
    画像の読み込みが終わると Image オブジェクトの onload プロパティに設定された イベントハンドラ(関数)が引数なしで呼び出されます。

  5. 画像の読み込みをブラウザに依頼する。
  6. image.src="画像へのパス";
    Imageオブジェクトの src プロパティに読み込みたい画像のパス(文字列)を設定すると、 ブラウザは画像の読み込みを開始します。 HTMLで<img>タグの要素のsrcプロパティで読み込む画像を設定したときの ブラウザの動作と似ています。


4. WebGLでテクスチャ画像を使用できるようにする

WebGLではテクスチャ画像をテクスチャユニットで管理します。 テクスチャユニットは少なくとも gl.TEXTURE0〜gl.TEXTURE7の8個が存在し、 同じ数だけ用意されたそれぞれの gl.TEXTURE_2D と対応しています。 テクスチャユニットに画像データを設定し、それをフラグメント シェーダに伝える手順を説明します。

  1. テクスチャオブジェクトを生成する。gl.createTexture()

  2. 指定したテクスチャユニットを有効にする。gl.activeTexture()
  3. WebGLでは複数のテクスチャを利用できます。 それぞれのテクスチャを管理するためにはテクスチャユニットを使います。 テクスチャユニットは少なくとも8個 (gl.TEXTURE0, gl.TEXTURE2, ..., gl.TEXTURE7)存在し、 それぞれに対応する TEXTURE_2D ユニットがあります。


  4. 有効にしたテクスチャユニットのターゲットにテクスチャオブジェクトをバインドする。gl.bindTexture()
  5. テクスチャユニットのターゲットには gl.TEXTURE_2D と gl.TEXTURE_CUBE_MAP があります。 2次元画像を用いる場合は gl.TEXTURE_2Dをターゲットに指定します。


  6. テクスチャオブジェクトにテクスチャパラメータを設定する。gl.texParameteri()
  7. テクスチャの拡大・縮小方法や、繰り返し方についての設定をします。

  8. テクスチャオブジェクトに画像データを設定する。gl.texImage2D()
  9. javascriptのImageオブジェクト内の画像データを、WebGLの テクスチャオブジェクトへと転送します。



5. フラグメントシェーダに、利用するテクスチャユニットの番号を渡す

フラグメントシェーダに、使用するテクスチャを保持しているテクスチャユニット番号を uniform変数を用いて渡します。1個の整数を渡すにはgl.uniform1i()を用います。


6. 頂点シェーダで、テクスチャ座標をフラグメントシェーダに転送する。

頂点シェーダでは、頂点のテクスチャ座標系での位置がattribute変数で 渡されてくるので、varying変数に代入してフラグメントシェーダに渡します。

頂点シェーダ用プログラム
attribute vec4 a_Position;
attribute vec2 a_TexCoord;
varying vec2 v_TexCoord;
...
    v_TexCoord = a_TexCoord;

7. フラグメントシェーダで、テクセルの色を取り出しフラグメントに設定する。

フラグメントシェーダで画像からテクセルの色を取り出し、対応するフラグメントに設定します。

テクスチャ画像を保持しているテクスチャユニットの番号はuniform変数で渡されています。 また、テクスチャ座標系でのフラグメントの位置は、補間された値がvarying変数で渡されています。

フラグメントシェーダ用プログラム
uniform sampler2D u_Sampler;
varying vec2 v_TexCoord;
...
    vec4型の変数 = texture2D(u_Sampler,v_TexCoord);


フラグメントシェーダの関数
vec4 texture2D(sampler2D, vec2)
 @param sampler2d テクスチャユニット番号
 @param vec2 テクスチャ座標の値
 @return vec4 テクセルの色。画像が用意されていない場合は(0.0,0.0,0.0,1.0)。
glの関数
createTexture()
  テクスチャ画像を管理するテクスチャオブジェクトを生成する。
  @return 作成されたテクスチャオブジェクト or null(エラーの場合)
activeTexture(texUnit )
  指定したテクスチャユニット texUnit を有効にする。
  @param  texUnit : 有効にするテクスチャユニット。gl.TEXTURE[0-7]
  @return なし
bindTexture(target , texture )
  有効になっているテクスチャユニットに対応するtarget に、テクスチャオブジェクトをバインドする。
  @param target  gl.TEXTURE_2D(2次元) or gl.TEXTURE_CUBE_MAP(3次元)
  @param texture  テクスチャオブジェクト
  @return なし
texParameteri(target , pname , param )
  有効になっているテクスチャユニットに対応するtarget の、pname パラメータ値を param に設定する。
  @param target  gl.TEXTURE_2D(2次元) or gl.TEXTURE_CUBE_MAP(3次元)
  @param pname  テクスチャパラメータ名
  @param param  テクスチャパラメータの値
  @return なし
テクスチャパラメータとデフォルト値
テクスチャパラメータ 説明 デフォルト値
gl.TEXTURE_MAG_FILTER テクスチャ画像の拡大方法 gl.LINEAR
gl.TEXTURE_MIN_FILTER テクスチャ画像の縮小方法 gl.NEAREST_MIPMAP_NINEAR
gl.TEXTURE_WRAP_S s軸方向の画像の繰り返し方法 gl.REPEAT
gl.TEXTURE_WRAP_T t軸方向の画像の繰り返し方法 gl.REPEAT
gl.TEXTURE_{MAG,MIN}_FILTERに指定できる値
gl.NEAREST フラグメントの座標に最も近いテクセルの値を使う
gl.LINEAR フラグメントの座標の周囲のテクセルの色の平均して使う
gl.TEXTURE_WRAP_{ST}_FILTERに指定できる値
gl.REPEAT 画像を繰り返し使用する
gl.MIRRORED_REPEAT 画像を反転させながら繰り返し使用する
gl.CLAMP_TO_EDGE 画像の端の色を使用する
texImage2D(target , level , internalformat , format , type , image )
  有効になっているテクスチャユニットに対応するtarget にバインドされたテクスチャオブジェクトに、
  javascriptの image オブジェクトの画像を転送する。
  @param target  gl.TEXTURE_2D(2次元) or gl.TEXTURE_CUBE_MAP(3次元)
  @param level  テクスチャがミップマップの場合はそのレベルを、それ以外の場合は0を指定する
  @param internalformat  テクスチャ画像の内部フォーマット。
  @param format  テクセルのフォーマット。internalformat と同じ値。
  @param type  テクセルのデータ型
  @param image  Imageオブジェクト
  @return なし
{intenal,}formatに指定できる値
説明
gl.RGBRGB(赤、緑、青)
gl.RGBARGBとA(アルファ値)
gl.ALPHA(0.0, 0.0, 0.0, アルファ値)
gl.LUMINANCE輝度
gl.LUMINANCE_ALPHA輝度とアルファ値
typeに指定できる値
説明
gl.UNSIGNED_BYTERBGそれぞれ符号なし1バイト
gl.UNSIGNED_SHORT_5_6_5RGBがそれぞれ5,6,5ビット
gl.UNSIGNED_SHORT_4_4_4_4RGBAがそれぞれ4ビット
gl.UNSIGNED_SHORT_5_5_5_1RGBAがそれぞれ5,5,5,1ビット

ParaeelLightCube6.htmlが平行光線の元でひとつの立体を扱う最後のプログラムでした。 ParallelLightCube6.html から最小限の変更で物体表面にテクスチャを貼りつけてみます。 ここではシェーディングを考慮していません。

Texture.html
*** ParallelLightCube6.html	Fri May 11 12:29:33 2018
--- Texture.html	Fri May 11 12:54:28 2018
***************
*** 14,25 ****
--- 14,27 ----
  attribute vec4 a_Position;\n\
  attribute vec4 a_Color;\n\
  attribute vec4 a_Normal;\n\
+ attribute vec2 a_TexCoord;\n\
  uniform mat4 u_MvpMatrix;\n\
  uniform mat4 u_NormalMatrix;\n\
  uniform vec3 u_LightColor;\n\
  uniform vec3 u_LightDirection;\n\
  uniform vec3 u_AmbientLight;\n\
  varying vec4 v_Color;\n\
+ varying vec2 v_TexCoord;\n\
  void main() {\n\
      gl_Position = u_MvpMatrix * a_Position;\n\
      vec3 normal = normalize(vec3(u_NormalMatrix * a_Normal));\n\
***************
*** 27,39 ****
      vec3 diffuse = u_LightColor * a_Color.rgb * nDotL;\n\
      vec3 ambient = u_AmbientLight * a_Color.rgb;\n\
      v_Color = vec4(diffuse+ambient,a_Color.a);\n\
  }\n\
  ";
  var FSHADER_SOURCE = "\
  precision mediump float;\n\
  varying vec4 v_Color;\n\
  void main() {\n\
!     gl_FragColor = v_Color;\n\
  }\n\
  ";
  function main() {
--- 29,44 ----
      vec3 diffuse = u_LightColor * a_Color.rgb * nDotL;\n\
      vec3 ambient = u_AmbientLight * a_Color.rgb;\n\
      v_Color = vec4(diffuse+ambient,a_Color.a);\n\
+     v_TexCoord = a_TexCoord;\n\
  }\n\
  ";
  var FSHADER_SOURCE = "\
  precision mediump float;\n\
+ uniform sampler2D u_Sampler;\n\
  varying vec4 v_Color;\n\
+ varying vec2 v_TexCoord;\n\
  void main() {\n\
!     gl_FragColor = texture2D(u_Sampler,v_TexCoord);\n\
  }\n\
  ";
  function main() {
***************
*** 46,55 ****
--- 51,63 ----
      var indices = tsuda.cube.indices;
      var colors = tsuda.get3Float32Array(0.0, 1.0, 0.0, vertices.length);
      var normals = tsuda.indexNormals(vertices,indices);
+     var texCoord = tsuda.cube.texCoord;
+     var texture = gl.createTexture();
  
      if (! tsuda.initArrayBuffer('a_Position',vertices,3,gl.FLOAT)) return;
      if (! tsuda.initArrayBuffer('a_Color',colors,3,gl.FLOAT)) return;
      if (! tsuda.initArrayBuffer('a_Normal',normals,3,gl.FLOAT)) return;
+     if (! tsuda.initArrayBuffer('a_TexCoord',texCoord,2,gl.FLOAT)) return;
  
      if (! tsuda.setElementArrayBuffer(indices)) return;
  
***************
*** 58,65 ****
      var u_LightColor = gl.getUniformLocation(gl.program, 'u_LightColor');
      var u_LightDirection = gl.getUniformLocation(gl.program, 'u_LightDirection');
      var u_AmbientLight = gl.getUniformLocation(gl.program, 'u_AmbientLight');
      if (! u_MvpMatrix || ! u_LightColor || ! u_LightDirection || ! u_AmbientLight
!         || ! u_NormalMatrix) {
          console.log('failed to get location of uniform variable');
          return;
      }
--- 66,75 ----
      var u_LightColor = gl.getUniformLocation(gl.program, 'u_LightColor');
      var u_LightDirection = gl.getUniformLocation(gl.program, 'u_LightDirection');
      var u_AmbientLight = gl.getUniformLocation(gl.program, 'u_AmbientLight');
+     var u_Sampler = gl.getUniformLocation(gl.program,'u_Sampler');
+ 
      if (! u_MvpMatrix || ! u_LightColor || ! u_LightDirection || ! u_AmbientLight
!         || ! u_NormalMatrix || ! u_Sampler ) {
          console.log('failed to get location of uniform variable');
          return;
      }
***************
*** 90,99 ****
      gl.enable(gl.DEPTH_TEST);
      gl.depthFunc(gl.LEQUAL);
  
!     gl.clearColor(0.0, 0.0, 0.0, 1.0); // black
!     gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
! 
!     gl.drawElements(gl.TRIANGLES, indices.length,gl.UNSIGNED_BYTE,0);
  }
  //]]>
    </script>
--- 100,119 ----
      gl.enable(gl.DEPTH_TEST);
      gl.depthFunc(gl.LEQUAL);
  
!     var image = new Image();
!     image.onload = function() {
!        gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL,1); // reverse y axis of texture image
!        gl.activeTexture(gl.TEXTURE0);
!        gl.bindTexture(gl.TEXTURE_2D,texture);
!        gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_MIN_FILTER,gl.LINEAR);
!        gl.texImage2D(gl.TEXTURE_2D,0,gl.RGB,gl.RGB,gl.UNSIGNED_BYTE,image);
!        gl.uniform1i(u_Sampler,0);
! 
!        gl.clearColor(0.0, 0.0, 0.0, 1.0); // black
!        gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
!        gl.drawElements(gl.TRIANGLES, indices.length,gl.UNSIGNED_BYTE,0);
!     };
!     image.src = "./sakura1024.jpg";
  }
  //]]>
    </script>


シェーディングを考慮したプログラムに変更しましょう。 頂点シェーダで計算したdiffuse成分とambient成分 の和が輝度(明るさ)となりますので、 この値をフラグメントシェーダに渡し、 フラグメントシェーダではテクセルの値に輝度を乗算してそのピクセルの色を決定します。

以下のプログラムでは、 ポリゴンの色は必要ないので、javascriptからWebGLに渡すのを止めています。 また、頂点シェーダ内で 「光の向きと面の法線から求めたデフューズ成分」と 「環境光からのくるアンビェント成分」を加算して輝度 (v_Brightness) としてフラグメントシェーダに渡しています。 フラグメントシェーダ内では、テクスチャからの色に輝度をそれぞれ 乗算して色を計算します。

Texture2.html
*** Texture.html	Fri May 11 12:54:28 2018
--- Texture2.html	Sat Jun 23 01:19:06 2018
***************
*** 12,18 ****
      //<![CDATA
  var VSHADER_SOURCE = "\
  attribute vec4 a_Position;\n\
- attribute vec4 a_Color;\n\
  attribute vec4 a_Normal;\n\
  attribute vec2 a_TexCoord;\n\
  uniform mat4 u_MvpMatrix;\n\
--- 12,17 ----
***************
*** 20,44 ****
  uniform vec3 u_LightColor;\n\
  uniform vec3 u_LightDirection;\n\
  uniform vec3 u_AmbientLight;\n\
- varying vec4 v_Color;\n\
  varying vec2 v_TexCoord;\n\
  void main() {\n\
      gl_Position = u_MvpMatrix * a_Position;\n\
      vec3 normal = normalize(vec3(u_NormalMatrix * a_Normal));\n\
      float nDotL = max(dot(u_LightDirection,normal),0.0);\n\
-     vec3 diffuse = u_LightColor * a_Color.rgb * nDotL;\n\
-     vec3 ambient = u_AmbientLight * a_Color.rgb;\n\
-     v_Color = vec4(diffuse+ambient,a_Color.a);\n\
      v_TexCoord = a_TexCoord;\n\
  }\n\
  ";
  var FSHADER_SOURCE = "\
  precision mediump float;\n\
  uniform sampler2D u_Sampler;\n\
- varying vec4 v_Color;\n\
  varying vec2 v_TexCoord;\n\
  void main() {\n\
!     gl_FragColor = texture2D(u_Sampler,v_TexCoord);\n\
  }\n\
  ";
  function main() {
--- 19,43 ----
  uniform vec3 u_LightColor;\n\
  uniform vec3 u_LightDirection;\n\
  uniform vec3 u_AmbientLight;\n\
  varying vec2 v_TexCoord;\n\
+ varying vec4 v_Brightness;\n\
  void main() {\n\
      gl_Position = u_MvpMatrix * a_Position;\n\
      vec3 normal = normalize(vec3(u_NormalMatrix * a_Normal));\n\
      float nDotL = max(dot(u_LightDirection,normal),0.0);\n\
      v_TexCoord = a_TexCoord;\n\
+     v_Brightness = vec4(u_LightColor * nDotL + u_AmbientLight,1.0);\n\
  }\n\
  ";
  var FSHADER_SOURCE = "\
  precision mediump float;\n\
  uniform sampler2D u_Sampler;\n\
  varying vec2 v_TexCoord;\n\
+ varying vec4 v_Brightness;\n\
  void main() {\n\
!     vec4 color = texture2D(u_Sampler,v_TexCoord);\n\
!     vec3 c = color.rgb * v_Brightness.rgb;\n\
!     gl_FragColor = vec4(c.rgb,color.a);\n\
  }\n\
  ";
  function main() {
***************
*** 49,61 ****
  
      var vertices = tsuda.cube.vertices;
      var indices = tsuda.cube.indices;
-     var colors = tsuda.get3Float32Array(0.0, 1.0, 0.0, vertices.length);
      var normals = tsuda.indexNormals(vertices,indices);
      var texCoord = tsuda.cube.texCoord;
      var texture = gl.createTexture();
  
      if (! tsuda.initArrayBuffer('a_Position',vertices,3,gl.FLOAT)) return;
-     if (! tsuda.initArrayBuffer('a_Color',colors,3,gl.FLOAT)) return;
      if (! tsuda.initArrayBuffer('a_Normal',normals,3,gl.FLOAT)) return;
      if (! tsuda.initArrayBuffer('a_TexCoord',texCoord,2,gl.FLOAT)) return;
  
--- 48,58 ----