床井研究室

戦力外通告

一口に仕事といってもいろんな側面があるとは思うのですが、だからと言って独りよがりなことばかりしていれば、評価を得ることはできません。そして評価が得られない状態が継続していると、当然その組織に貢献していないと見なされ、戦力外を通告されることになります1

固定機能の追加の限界とプログラマブルシェーダ

Dot3 バンプマッピングシャドウマッピングは、画素単位の陰影付けや影付け処理を固定機能のハードウェア上に実装するための、非常に巧みな拡張機能だと思います。しかし、ユーザ(デザイナ、プログラマ)の多様な発想から発せられる様々な要求をこのような形で実装し続けることには、遠からず限界がきます。したがって、ユーザの発想をユーザ自身の手によって実装できるような仕組みを用意することは、当然の流れなのでしょう。

プログラマブルシェーダの導入によって、レンダリング時における頂点単位の処理や画素単位の処理がユーザに解放されました。当初このプログラミングには、アセンブリ言語が用いられていました。しかし、他のプログラミング言語と同様、プログラムの開発効率や可読性、それに互換性が重視された結果、現在ではここにも高級言語が用いられるようになっています。

このような高級言語として、Windows の DirectX 9 には HLSL (High Level Shading Language) や、nVIDIA が開発した(HLSL とほぼ同じで OpenGL でも使える)Cg があります。また OpenGL には、Cg の他に 3DLabs2 が開発し OpenGL 2.0 の標準機能に取り込まれた GLSL (OpenGL Shading Language) があります。ここでは GLSL について簡単に説明します。

GLSL (OpenGL Shading Language)

GLSL は OpenGL の 1.5 の拡張機能として実装され、OpenGL 2.0 で標準機能となりました。3DLabs の Wildcat VP や Realism、nVIDIA の GeForce FX 以降、ATI の RADEON 9600 以降のビデオカードであれば、最新のドライバを使用することにより、GLSL が使用できます。

ただし OpenGL の 1.5 と 2.0 では、GLSL をサポートするための API の関数名が微妙に異なります。関数名の末尾の “ARB” の有無以外にも変化があることに加えて、引数のデータ型も少し違っていたりします。現在使用しているノートパソコンが OpenGL 1.5 だったのでどちらを説明すべきか少し悩んだのですが、やはりこれまでのやり方?に倣って、GLSL を標準機能としている OpenGL 2.0 をもとに説明します。

GLSL についての詳細についてはオレンジブックか、もしくは GLSL の仕様書OpenGL の仕様書を参照してください。

シェーダプログラムの読み込み

シェーダのプログラミングは、頂点単位の処理を行うバーテックスシェーダと、画素単位の処理を行うフラグメントシェーダの二つについて行います。これらは独立したプログラムですが、バーテックスシェーダで処理した結果をフラグメントシェーダで使用するので、この二つは対にして取り扱う必要があります。

GLSL のシェーダプログラミングで私が最初に驚いたのは、シェーダのソースプログラムを実行時にコンパイルすることでした。このためにシェーダプログラムのコンパイラが、GLSL をサポートする API、すなわちビデオカードのドライバに含まれています。これは私のドライバに対するイメージから大きくかけ離れたものでした。

毎回実行時にコンパイルするのは非効率的のように思えますが、こうすることによりビデオカードの機能の差を隠蔽し、使用するビデオカードにとって最も効率的なシェーダプログラムを生成することができます。なお、Cg でも実行時にコマンドラインコンパイラ (cgc) を呼び出す方法が推奨されていたりします3

GLSL のシェーダプログラムを利用する手順は、以下のようになります。

  1. バーテックスシェーダとフラグメントシェーダのシェーダオブジェクトを作成します (glCreateShader())。
  2. 作成したそれぞれのシェーダオブジェクトに対してソースプログラムを読み込みます (glShaderSource())。
  3. 読み込んだソースプログラムをコンパイルします (glCompileShader())。
  4. プログラムオブジェクトを作成します (glCreateProgram())。
  5. プログラムオブジェクトに対してシェーダオブジェクトを登録します (glAttachShader())。
  6. シェーダプログラムをリンクします (glLinkProgram())。
  7. シェーダプログラムを適用します (glUseProgram())。

以下、次のサンプルプログラムに追加する形で、この手順を説明します。

オリジナルプログラムの生成画像1 オリジナルプログラムの生成画像2

プログラムはテクスチャマッピング入門の第1回で使ったのと同じ、1枚の四角形をくるくる回すものです。ただし、材質や光源の設定を変えてあります。

プログラムオブジェクトの識別子

まず、プログラムオブジェクトの識別子(ハンドル)を格納する変数 gl2Program を用意しておきます。ファイル glsl.h は OpenGL 関連のヘッダファイルと glsl.cpp で定義している Windows 用の API の関数ポインタ変数、それに補助的に使用する関数の宣言を行っています。

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

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

/* 標準ライブラリ */
#include <stdio.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;

プログラムオブジェクトの作成

最初に GLSL 関連の OpenGL の API が使えるようにします。この処理は Windows のみ必要です。Windows は、標準では OpenGL のバージョン 1.1 しかサポートしていないので、GPU がそれ以降の機能を持っていても、そのままでは使用できません。そこで、GPU のドライが提供している機能を、プログラムから呼び出せるようにします。

関数 glslInit() は glsl.cpp で定義しており、GLSL で使用する API のエントリポイントを、関数ポインタ変数に格納します。

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

次に、glCreateShader() を使って、バーテックスシェーダとフラグメントシェーダのそれぞれのシェーダオブジェクトを作成します。

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

そして関数 readShaderSource() を使って、ファイルからシェーダのソースプログラムを読みます。この関数は読み込んだソースプログラムを glShaderSource() に渡します。これは glsl.cpp で定義しています。

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

ソースプログラムが読み込めたら、glCompileShader() を使って glShaderSource() に渡されたソースプログラムをコンパイルし、バーテックスシェーダとフラグメントシェーダのシェーダオブジェクトを作成します。

コンパイルに成功したかどうかは、glGetShaderiv() を使って、変数 compiled に得ることができます。この内容が GL_FALSE なら、コンパイルに失敗したことになります。関数 printShaderInfoLog() はシェーダのコンパイル時のメッセージを取り出して出力します。これも glsl.cpp で定義しています。

  /* シェーダプログラムのコンパイル結果 */
  GLint compiled;

  /* バーテックスシェーダのソースプログラムのコンパイル */
  glCompileShader(vertShader);
  glGetShaderiv(vertShader, GL_COMPILE_STATUS, &compiled);
  printShaderInfoLog(vertShader);
  if (compiled == GL_FALSE) {
    fprintf(stderr, "Compile error in vertex shader.\n");
    exit(1);
  }

  /* フラグメントシェーダのソースプログラムのコンパイル */
  glCompileShader(fragShader);
  glGetShaderiv(fragShader, GL_COMPILE_STATUS, &compiled);
  printShaderInfoLog(fragShader);
  if (compiled == GL_FALSE) {
    fprintf(stderr, "Compile error in fragment shader.\n");
    exit(1);
  }

シェーダのソースプログラムのコンパイルに成功したら、glCreateProgram() を使ってプログラムオブジェクトを作成し、glAttachShader() を使って、それにシェーダオブジェクトを登録します。この時点でシェーダオブジェクトは不要になるので、削除マークを付けておきます。

  /* プログラムオブジェクトの作成 */
  gl2Program = glCreateProgram();

  /* シェーダオブジェクトのシェーダプログラムへの登録 */
  glAttachShader(gl2Program, vertShader);
  glAttachShader(gl2Program, fragShader);

  /* シェーダオブジェクトに削除マークを付ける */
  glDeleteShader(vertShader);
  glDeleteShader(fragShader);

glLinkProgram() によってシェーダプログラムをリンクします。リンクが成功したかどうかは、glGetProgramiv() を使って変数 linked に得ます。この内容が GL_FALSE なら、リンクに失敗したことになります。関数 printProgramInfoLog() は glsl.cpp で定義しており、シェーダのリンク時に出力されたメッセージを取り出し、標準エラー出力に出力します。

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

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

プログラムオブジェクトの適用

作成したプログラムオブジェクトを使った描画を行うには、図形の描画の前に glUseProgram() を使って、使用するプログラムオブジェクトを指定します。

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

図形の描画が終わったら、glUseProgram(0) を使ってシェーダプログラムの適用を解除します。これにより、従来のシェーダを使わない描画方法に戻します。

  /* 画面クリア */
  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

  /* シーンの描画 */
  scene();

  /* シェーダプログラムの適用解除 */
  glUseProgram(0);

  /* ダブルバッファリング */
  glutSwapBuffers();
}

シェーダのソースプログラム

とりあえず、ここでは非常に簡単なシェーダのプログラムを用意しておきます。詳しい説明は次回以降に行います。

以下はバーテックスシェーダのソースプログラム (simple.vert) です。プログラムのエントリポイントとなる関数名は C や C++ 同様 main() ですが、戻り値を返すわけではないので、データ型は void です。

バーテックスシェーダでは、1個1個の頂点に対してこの処理が行われます。gl_Vertex はプログラム中で与えられた頂点の座標値であり、これに GLSL の組み込み変数 gl_ModelViewProjectionMatrix、すなわちモデルビュー変換行列と透視変換行列の積を掛けたものを gl_Position に格納します。gl_Position の値が、その頂点のスクリーン上での位置になります。

#version 120

// simple.vert

void main()
{
  // 頂点位置
  gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
}

なお、一般にはこの積のかわりに ftransform() という GLSL の組み込み関数を用います4

#version 120

// simple.vert

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

次はフラグメントシェーダのソースプログラム (simple.frag) です。この処理は画素単位に実行されます。ここで陰影計算などを行い、gl_FragColor に色を格納して、その画素に色をつけます。

#version 120

// simple.frag

void main ()
{
  // フラグメントの色
  gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}

このプログラムではフラグメント(画素)の色を赤色の定数 (1,0,0,1) にしているので、描画される四角形は陰影のない赤になってしまいます。

シェーダを使った生成画像1 シェーダを使った生成画像2

  1. もっと組織に貢献する形で行動を最適化しなければと考えています。ちょっと頑張ってみます。(2013 年 7 月 3 日追記) 

  2. 3DLabs は 2002 年 3 月 11 日に Creative Technology に買収され、2006 年 2 月 24 日にプロフェッショナル向け3Dグラフィックス事業から撤退させられました。”3dlabs.com” というドメインは売りに出されていました。(2026 年 5 月 20 日追記) 

  3. GLSL で大きな uniform の配列を取ったときに cgc のエラーが出てきて面食らったことがあります。(2026 年 5 月 21 日追記) 

  4. gl_ModelViewProjectionMatrixftransform() も、以前の(プログラマブルではない)固定シェーダの設定値を引き継ぐものなので、今は使われていません。(2026 年 5 月 21 日追記)