床井研究室

昭和テイスト

研究室のミーティングで学生さんに対して「当たり前田のクラッカー!」っていう具合に突っ込みを入れたら、「昭和テイストだなぁー」とか「世代が違いますよ、世代が」とか散々な言われ方をしました1。私、やっぱり旧い人なんでしょうか?(聞くまでもないか)そういえば以前、奥さんに「おさじとって」と言ったら、「はい、スプーン」と訂正されてしまいました。昭和は遠くなったもんだ(違

テクスチャを参照する

シェーダプログラムの中でもテクスチャを参照することができます。もちろん、マルチテクスチャが使えます。複数のテクスチャを組み合わせた処理を手続きで書けるってあたりが、シェーダプログラミングの醍醐味でしょう。

とりあえず、前回のプログラムにテクスチャに使う画像を読み込む処理を追加します。テクスチャには、以前に使った以下の画像を使います。なお本題とは関係ありませんが、以下のプログラムでは OpenGL 1.4 で標準機能となったミップマップの自動生成機能を使用しています。

箱にマッピングするテクスチャ

これにテクスチャを読み込むコードを追加します。まず、テクスチャのサンプラの uniform 変数の場所を保持する変数と、テクスチャのサイズ、テクスチャファイル名を決めておきます。

/* OpenGL / GLSL 関連の宣言 */
#include "glsl.h"

/* トラックボール処理用関数の宣言 */
#include "trackball.h"

/* 標準ライブラリ */
#include <stdio.h>
#include <stdlib.h>

/* 1 ならティーポットを描く */
#define DRAW_TEAPOT 0

/*
** 光源
*/
static const GLfloat lightpos[] = { 0.0f, 0.0f, 5.0f, 1.0f }; /* 位置    */
static const GLfloat lightcol[] = { 1.0f, 1.0f, 1.0f, 1.0f }; /* 直接光強度 */
static const GLfloat lightamb[] = { 0.1f, 0.1f, 0.1f, 1.0f }; /* 環境光強度 */

/*
** プログラムオブジェクト
*/
static GLuint gl2Program;

/*
** テクスチャのサンプラの uniform 変数の場所
*/
static GLuint textureLoc;

/*
** テクスチャ
*/
#define TEXWIDTH  256                               /* テクスチャの幅    */
#define TEXHEIGHT 256                               /* テクスチャの高さ   */
static const char texture_file[] = "dot.raw";       /* テクスチャファイル名 */

初期化処理では、読み込むシェーダのソースプログラムのファイル名を texture.vert と texture.frag に変更しておきます。

/*
** 初期化
*/
static void init()
{
  /* GLSL の初期化 */
  if (glslInit()) exit(1);

  /* シェーダオブジェクトの作成 */
  GLuint vertShader = glCreateShader(GL_VERTEX_SHADER);
  GLuint fragShader = glCreateShader(GL_FRAGMENT_SHADER);

  /* シェーダのソースプログラムの読み込み */
  if (readShaderSource(vertShader, "texture.vert")) exit(1);
  if (readShaderSource(fragShader, "texture.frag")) exit(1);

シェーダプログラムのリンクに成功したら、プログラムオブジェクトからテクスチャのサンプラの uniform 変数の場所を取り出します。これには glGetUniformLocation() 関数を使います。

  /* シェーダプログラムのリンク結果 */
  GLint linked;

  /* シェーダプログラムのリンク */
  glLinkProgram(gl2Program);
  glGetProgramiv(gl2Program, GL_LINK_STATUS, &linked);
  printProgramInfoLog(gl2Program);
  if (linked == GL_FALSE) {
    fprintf(stderr, "Link error.\n");
    exit(1);
  }

  /* テクスチャのサンプラの uniform 変数の場所を得る */
  textureLoc = glGetUniformLocation(gl2Program, "texture");

テクスチャユニット0を指定してテクスチャを作成します。

  /* テクスチャユニット0を指定する */
  glActiveTexture(GL_TEXTURE0);

  /* テクスチャオブジェクトの作成と結合 */
  GLuint tex;
  glGenTextures(1, &tex);
  glBindTexture(GL_TEXTURE_2D, tex);

  /* ミップマップを自動生成する */
  glTexParameteri(GL_TEXTURE_2D, GL_GENERATE_MIPMAP, GL_TRUE);

  /* テクスチャを拡大・縮小する方法の指定 */
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);

  /* テクスチャの繰り返し方法の指定 */
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);

テクスチャファイルから画像を読み込み、テクスチャに格納します。

  /* テクスチャ画像はワード単位に詰め込まれている */
  glPixelStorei(GL_UNPACK_ALIGNMENT, 4);

  /* テクスチャの読み込みに使う配列 */
  GLubyte texture[TEXHEIGHT][TEXWIDTH][4];

  /* テクスチャ画像の読み込み */
  FILE *fp = fopen(texture_file, "rb");
  if (fp != NULL) {
    fread(texture, sizeof texture, 1, fp);
    fclose(fp);
  }
  else {
    perror(texture_file);
  }

  /* テクスチャの割り当て */
  glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, TEXWIDTH, TEXHEIGHT, 0,
    GL_RGBA, GL_UNSIGNED_BYTE, texture);

  /* 初期設定 */
  glClearColor(0.3f, 0.3f, 1.0f, 0.0f);
  glEnable(GL_DEPTH_TEST);
  glDisable(GL_CULL_FACE);

  /* 光源の初期設定 */
  glEnable(GL_LIGHTING);
  glEnable(GL_LIGHT0);
  glLightfv(GL_LIGHT0, GL_DIFFUSE, lightcol);
  glLightfv(GL_LIGHT0, GL_SPECULAR, lightcol);
  glLightfv(GL_LIGHT0, GL_AMBIENT, lightamb);
  glLightModeli(GL_LIGHT_MODEL_LOCAL_VIEWER, GL_TRUE);
}

図形描画の際にテクスチャ座標を設定しておきます。シェーダプログラムを使う場合は、glEnable( GL_TEXTURE_2D ) を実行する必要はありません。

/*
** シーンの描画
*/
static void scene()
{
  /* 材質 */
  static const GLfloat diffuse[] = { 0.6f, 0.1f, 0.1f, 1.0f };
  static const GLfloat specular[] = { 0.3f, 0.3f, 0.3f, 1.0f };

  /* 材質の設定 */
  glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE, diffuse);
  glMaterialfv(GL_FRONT_AND_BACK, GL_SPECULAR, specular);
  glMaterialf(GL_FRONT_AND_BACK, GL_SHININESS, 100.0f);

#if DRAW_TEAPOT
  /* ティーポットを描く */
  glutSolidTeapot(1.0);
#else
  /* 1枚の4角形を描く */
  glNormal3d(0.0, 0.0, 1.0);
  glBegin(GL_QUADS);
  glTexCoord2d(0.0, 1.0);
  glVertex3d(-1.0, -1.0,  0.0);
  glTexCoord2d(1.0, 1.0);
  glVertex3d( 1.0, -1.0,  0.0);
  glTexCoord2d(1.0, 0.0);
  glVertex3d( 1.0,  1.0,  0.0);
  glTexCoord2d(0.0, 0.0);
  glVertex3d(-1.0,  1.0,  0.0);
  glEnd();
#endif
}

そしてシェーダプログラムを適用し、テクスチャのサンプラの uniform 変数にテクスチャユニット0を指定してから、図形を描画するようにします。uniform 変数 (texture) の値は、glGetUniformLocation() を使って得た uniform 変数の場所 (textureLoc) に対して、glUniform*() 関数を使って設定します。

/****************************
** GLUT のコールバック関数 **
****************************/

static void display()
{
  /* シェーダプログラムの適用 */
  glUseProgram(gl2Program);

  /* テクスチャのサンプラにテクスチャユニット0を指定する */
  glUniform1i(textureLoc, 0);

  /* モデルビュー変換行列の設定 */
  glMatrixMode(GL_MODELVIEW);
  glLoadIdentity();

  ...
}

これでテクスチャマッピングの準備は完了です。試しに関数 display() の最初にある glUseProgram( gl2Program ); をコメントアウトしてプログラムをコンパイル・実行し、テクスチャが正しく貼れるか確かめてください(確かめたらプログラムを元に戻してください)。

シェーダプログラムの作成

シェーダプログラムは前回作成した Phong シェーディングのものをもとにして作成します。phong.vert と phong.frag をそれぞれ texture.vert と texture.frag というファイル名にコピーしてください。

バーテックスシェーダ

バーテックスシェーダでは、テクスチャ変換行列 gl_TextureMatrix[0] を処理対象の頂点のテクスチャ座標 gl_MultiTexCoord0 に掛けて、組み込み varying 変数 gl_TexCoord[0] に代入します。gl_MultiTexCoord0 はテクスチャユニット0に設定されたテクスチャ座標です。gl_TexCoord は配列変数になっていますが、添え字の番号とテクスチャユニットは無関係(ただし総数は同じ)なので、gl_TexCoord のどの要素にどのテクスチャユニットのテクスチャ座標を入れても構いません。

#version 120

// texture.vert

// ラスタライザに送る頂点の位置
varying vec4 position;

// ラスタライザに送る頂点の法線ベクトル
varying vec3 normal;

void main()
{
  // 頂点のクリッピング座標値
  gl_Position = ftransform();

  // 頂点のワールド座標値
  position = gl_ModelViewMatrix * gl_Vertex;

  // 法線ベクトル
  normal = normalize(gl_NormalMatrix * gl_Normal);

  // テクスチャ座標
  gl_TexCoord[0] = gl_TextureMatrix[0] * gl_MultiTexCoord0;
}

なお、テクスチャ変換行列を使わない場合は、gl_TextureMatrix[0] を gl_MultiTexCoord0 にかける必要はありません。

フラグメントシェーダ

2次元テクスチャは GLSL の組み込み関数 texture2DProj() を使ってサンプリングします。変数 texturesampler2D 型の uniform 変数で、どのテクスチャユニットからどういう方法でテクスチャをサンプリングするかを指定します。この変数 texture の値(すなわちテクスチャユニット番号)の設定は、アプリケーションプログラム側で行います。 以下のプログラムでは、gl_TexCoord[0] に格納されているテクスチャ座標をテクスチャ変換行列 gl_TextureMatrix[0] により変換し、その結果を使って texture で指定されたテクスチャユニットが保持するテクスチャをサンプリングします。そして、サンプリングした色 color を使って拡散反射光強度と環境光の反射光強度を計算します。

#version 120

// texture.frag

// ラスタライザから受け取る頂点の位置の補間値
varying vec4 position;

// ラスタライザから受け取る頂点の法線ベクトルの補間値
varying vec3 normal;

// テクスチャのサンプラ
uniform sampler2D texture;

void main ()
{
  // 法線ベクトル
  vec3 fnormal = normalize(normal);

  // 光線ベクトル
  vec3 light = normalize((gl_LightSource[0].position * position.w
    - gl_LightSource[0].position.w * position).xyz);

  // 視線ベクトル
  vec3 view = -normalize(position.xyz);

  // 中間ベクトル
  vec3 halfway = normalize(light + view);

  // 拡散反射率
  float diffuse = max(dot(fnormal, light), 0.0);

  // 鏡面反射率
  float specular = pow(max(dot(fnormal, halfway), 0.0), gl_FrontMaterial.shininess);

  // テクスチャから画素の色を得る
  vec4 color = texture2DProj(texture, gl_TexCoord[0]);

  // フラグメントの色
  gl_FragColor = gl_LightSource[0].ambient * color
               + gl_LightSource[0].diffuse * diffuse * color
               + gl_FrontLightProduct[0].specular * specular;
}

もちろん、gl_LightSource[0].diffusecolor * gl_LightSource[0].ambientcolor でくくることができます。あんまり気にする必要はないと思いますけど。

  // フラグメントの色
  gl_FragColor = (gl_LightSource[0].ambient
               + gl_LightSource[0].diffuse * diffuse) * color
               + gl_FrontLightProduct[0].specular * specular;

以上によりシェーダプログラムの中でテクスチャを参照することが可能になります。

Phong シェーディングによるテクスチャを貼った四角形 Phong ーシェーディングによるテクスチャを貼った四角形を斜めから見たところ Phong シェーディングによるテクスチャを貼ったティーポット

  1. いま思えば、なんでこの人たちこんなに古いことを知っていたのか、親に聞いていたとしても、ちょっと謎。(2026 年 5 月 23 日追記)