«第27回 シャドウマッピング 最新 第2回 Gouraud シェーディングと Phong シ..»

床井研究室

※このブログは遅くとも 2027 年 3 月に管理者の定年退職により閉鎖します (移転先は管理者本人共々模索中)

■ 2005年10月06日 [OpenGL][GLSL] 第1回 シェーダプログラムの読み込み

2013年09月18日 20:21更新

戦力外通告

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

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

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

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

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

このような高級言語として,Windows の DirectX 9 には HLSL (High Level Shading Language) や,nVIDIA が開発した(HLSL とほぼ同じで OpenGL でも使える)Cg があります.また OpenGL には,Cg の他に 3DLabs が開発し 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 2.0 の仕様書を参照してください.

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

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

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

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

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

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

以下,次のサンプルプログラムに追加する形で,この手順を説明します.このアーカイブファイルには,Makefile (Vine Linux 6.1),Xcode 3 のプロジェクトファイル (Mac OS X),および Visual Studio 2010 のソリューションファイル (Windows) を添付しています.

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

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

オブジェクトの識別子

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

#include <stdio.h>
#include <stdlib.h>
#include <math.h>
 
#if defined(WIN32)
//#  pragma comment(linker, "/subsystem:\"windows\" /entry:\"mainCRTStartup\"")
#  include "glut.h"
#  include "glext.h"
#elif defined(__APPLE__) || defined(MACOSX)
#  include <GLUT/glut.h>
#else
#  define GL_GLEXT_PROTOTYPES
#  include <GL/glut.h>
#endif
 
/*
** 光源
*/
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 }; /* 環境光強度 */
 
/*
** シェーダ
*/
#include "glsl.h"
static GLuint vertShader;
static GLuint fragShader;
static GLuint gl2Program;
 
・・・

シェーダオブジェクトの作成

シェーダプログラムのコンパイルの結果は変数に取り出すので,そのための変数を用意しておきます.

・・・
 
/*
** 初期化
*/
static void init(void)
{
  /* シェーダプログラムのコンパイル/リンク結果を得る変数 */
  GLint compiled, linked;
  
  /* 初期設定 */
  glClearColor(0.3, 0.3, 1.0, 0.0);
  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);

関数 glslInit() は glsl.cpp で定義しており,GLSL で使用する API のエントリポイントを,関数ポインタ変数に格納します.この作業は Windows でのみ必要です.

  /* GLSL の初期化 */
  if (glslInit()) exit(1);

まず glCreateShader() を使ってシェーダオブジェクトを作成し,その識別子を得ます.そしてそれらの識別子に対してシェーダのソースプログラムを読み込みます.関数 readShaderSource() は glsl.cpp で定義しており,ファイルからソースプログラムを読み込んだ後,glShaderSource() を呼び出します.

  /* シェーダオブジェクトの作成 */
  vertShader = glCreateShader(GL_VERTEX_SHADER);
  fragShader = glCreateShader(GL_FRAGMENT_SHADER);
  
  /* シェーダのソースプログラムの読み込み */
  if (readShaderSource(vertShader, "simple.vert")) exit(1);
  if (readShaderSource(fragShader, "simple.frag")) exit(1);

読み込んだシェーダのソースプログラムを,glCompileShader() を使ってコンパイルします.コンパイルが成功したかどうかは,glGetShaderiv() を使って変数 compiled に得ます.この内容が GL_FALSE なら,コンパイルに失敗したことになります.関数 printShaderInfoLog() は glsl.cpp で定義しており,シェーダのコンパイル時に出力されたメッセージを取り出し,標準エラー出力に出力します.

  /* バーテックスシェーダのソースプログラムのコンパイル */
  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);
  }

シェーダのソースプログラムのコンパイルに成功したら,プログラムオブジェクトを作成し,シェーダオブジェクトを登録します.この時点でシェーダオブジェクトは不要になるので,削除してしまいます.

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

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

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

最後に,glUseProgram() を使って,これから後の図形描画にシェーダプログラムを使用するようにします.

  /* シェーダプログラムの適用 */
  glUseProgram(gl2Program);
}
 
・・・

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

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

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

バーテックスシェーダでは,1個1個の頂点に対してこの処理が行われます.gl_Vertex はプログラム中で与えられた頂点の座標値であり,これに gl_ModelViewProjectionMatrix,すなわちモデルビュー変換行列と透視変換行列の積を掛けたものを gl_Position に格納します.gl_Position の値が,その頂点のスクリーン上での位置になります.なお,一般にはこの積のかわりに ftransform() という関数を用います.

// simple.vert
 
void main(void)
{
  gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
}

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

// simple.frag
 
void main (void)
{
  gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}

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

シェーダを使った生成画像1 シェーダを使った生成画像2
コメント(6) [コメントを投稿する]
いち 2005年10月18日 12:52

床井先生のこのページは我々の大事な戦力になっています.床井先生がこのお仕事をされていなかったら,我々は今の10倍は苦労していたと思います.

とこ 2005年10月18日 22:56

ありがとうございます.つまらない泣き言を書いたばっかりにご心配をおかけしてしまったみたいで,申し訳ありません.実は他の方からも,励ましのメールを頂きました.感謝感激です.その上,いちさんに評価してもらえたことは,本当に励みになります.これからも自分のペースで地道にやっていきますね.

EKISUKE 2012年12月16日 23:52

プログラマブルシェーダの勉強をしようと探してて、OpenGLを使ってやっているとのことだったので、拝見させていただきました。<br>ソースもあってわかりやすいのですが、予備知識がないせいか、あまり理解できてないです^^;<br>とりあえず、わからなくても、打ってみるということでも、力になりますかね?

とこ 2012年12月24日 13:42

EKISUKE さま,コメントありがとうございます.<br>リプライが遅くなり申し訳ありません.<br>そのまま打ち込んでみてとりあえず動きましたら,あとはご自身で数値を変えたり処理を追加したりして,結果がどのように変わるか見て頂けるといいかと思います.何をしたらどうなるかという経験の積み重ねが,理解を深めて「勘」を身につけることにつながるのではないかと思います.

EKISUKE 2013年09月02日 15:02

結局シェーダの勉強をそのとき行う時間がなく、あきらめてしまったのですが、今改めて、学校でDirectXのシェーダの勉強をしていて、自分の作っているOpenGLのゲームにもシェーダを使いたく、glewを使ってのシェーダをやろうと思いいろんなページを見ながらやっています。<br>ここのソースはわかりやすく、コメントが細かく書かれているので助かっています。<br>このように丁寧に解説していただきありがとうございます。<br>これからも参考にさせていただきます。

とこ 2013年09月18日 20:21

すみませーん、ここんとこ忙しくて、見逃してました。<br>ありがとうございます。がんばってください。<br>私もがんばります。いろいろ。


編集 «第27回 シャドウマッピング 最新 第2回 Gouraud シェーディングと Phong シ..»