メモ代わり

Feed Rss

GLSL による空間の変換と描画

07.26.2011, know how, by .

Overview

OpenGL と DirectX などの 3D API を利用する場合、画面の表示範囲は -1.0 ~ +1.0 になります。

たとえば 1920×1080 ピクセルの画面で、 2D の場合はそのままピクセル数と位置が一致しますが、 3D の場合は X と Y 座標が -1.0 ~ +1.0 になります。また、 Z 座標は +0.0 ~ +1.0 になります。これらは画面やウィンドウサイズに関わらず一定になります。
(ウィンドウモードのときで、ウィンドウサイズ(レンダリング画面サイズ)が変わると、デバイスの再生成が必要になります。割と面倒な処理なので変更できないようにしてしまうケースが多いようです。)

また、いわゆるポリゴンは 2D でのビットマップとは違い、輪郭のみのベクトルデータで表現されます。
その 3D 物体を -1.0 ~ +1.0 に収めるために、主に 4×4 の行列を使います。行列を使い、物体を動かしたり、歪めたりして、画面内に収めます。

物体を空間内に設置するための行列を、ワールド行列もしくはモデル行列と呼ばれる事が多いようです。
その物体を見える範囲に設置するための行列を、ビュー行列もしくはカメラ行列と呼ばれる事が多いようです。
見える範囲を、 -1.0 ~ +1.0 の範囲内に収め、近くのものは大きく、遠くのものは小さく表示させるための行列を、プロジェクション行列と呼ぶようです。

空間の変換を横から見る


ポリゴンモデルを空間へ設置。各モデルごとの座標が、世界の座標に変換されます。


空間をカメラ内に収め、世界がカメラ中心になります。


視野角と、見える距離を設定して、画面に収まるようになります。視野角は画面の縦横比によって上下角と左右角が若干変わります。


画面に収まった図。奥のものほど小さくなると立体的に見えるようになります。
ちゃんと縦横の幅は -1.0~+1.0 であり、奥行きは 0.0~+1.0 のハコに収まりました。

真正面から見ると手前の物体で奥の物体が隠れるようになり、より立体感が増します。

Require

幾何(ジオメトリ)に関する知識は必要ありません。数学も殆ど不要です。どの関数を使ったらどのように変化するか、どの値をいじったらどのように変化するか、というのが重要です。

4×4 の行列を操作する関数はすでに山ほどあるので省略します。 GoogleCode で matrix.cpp などで検索すると良いでしょう。

行列操作とよくある関数

    ワールド基本操作、 XYZ 軸ごとにあります。

  • 何も変化しない Identity
  • 軸の回転 Rotate
  • 拡大縮小 Scale
  • 平行移動 Translate
  • 行列のかけざん Multiple
  • 初期設定などに使う操作

  • ビュー(カメラ)行列生成 LookAtLH
  • プロジェクション(パースペクティブ)行列生成 PerspectiveFov
  • その他

  • 逆行列 Inverse
  • 転置 Transpose
  • ベクトルの変換 Transform

Windows であれば D3DX に含まれる算術 API を使ってしまうのが簡単です。どの処理も非常に重く、利用率も高いので、最終的には各プラットフォーム毎に最適化された関数を使うようにしましょう。

Non Transform

まず何も変換をしない 2D のような描画をしてみます。一見役に立たないように見えて HUD などの 2D 描画のためにも必要です。

何かしらファイルからポリゴンデータを読み込むと見栄えが良いのですが、今回は三角形ひとつでやります。

まず頂点データ。

    float vertex[9]=
    { 0.0f,  0.5f, 0.0f
    , 0.8f, -0.3f, 0.0f
    ,-0.5f, -0.3f, 0.0f};

そして頂点バッファ作成と書き込み。頂点バッファは VRAM へ転送する必要があり、このような作業が必要です。

GLuint buffer_;
glGenBuffers(1,&buffer_);
glBindBuffer(GL_ARRAY_BUFFER, buffer_);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertex),vertex, GL_STATIC_DRAW);

これにて頂点データを VRAM へ転送は完了しました。 vertex 変数の役目は終了です。

次に描画のための GLSL コード。

attribute highp vec3 in_position;
void main(void){
  highp vec4 position=vec4(in_position,1);
  gl_Position=position;
}
void main(void){
mediump vec4 color=vec4(1.0,1.0,1.0,1.0);
   gl_FragColor=color;
}

前者が VertexShader になり、後者が FragmentShader になります。
それぞれコンパイルして ProgramObject に設定します。コンパイルのためのコードは省きます。

int vertex_offset=0;
glBindAttribLocation(po, vertex_offset++, "in_position");

glBindAttribLocation は glAttachShader 後かつ glLinkProgram 前でなければいけません。第二引数に設定した値は描画前に使います。 position 以外にも normal や color, uv など順に値を設定することになります。

描画コード

glBindBuffer(GL_ARRAY_BUFFER,vertex_buffer_);
int vertex_offset=0;
glEnableVertexAttribArray(vertex_offset);
glVertexAttribPointer(vertex_offset,3,GL_FLOAT,GL_FALSE,stride,(void*)pointer_offset);
glDrawArrays(GL_TRIANGLES,0,triangles);

stride は座標ひとつのバイト数です。今回は sizeof(float)*3 です。 pointer_offset は頂点バッファの先頭からそのデータへのオフセットをバイト数で表します。今回は 0 です。 GL_TRIANGLES は三角形リストを描画する命令です。 triangles に三角形の数である 1 を設定します。

以上をビルドし、実行すると次のようになります。

画面の座標が縦横それぞれ -1.0 ~ +1.0 なので、三角形の上の点が (0.0f, 0.5f, 0.0f) 右の点が (0.8f, -0.3f, 0.0f) 左の点が (-0.5f, -0.3f, 0.0f) になります。

色は FragmentShader にて指定している vec4(1.0,1.0,1.0,1.0); がそれぞれ RGBA に対応するのでたとえば三つ目の要素を 0.0f にすると三角形は黄色になります。

まとめ

  • 頂点データは X,Y,Z の順に float で表す。エンディアンは気にしなくて良い。
  • 三角形ひとつを表すのに頂点データが三つ要る。
  • シェーダで頂点データを受け取るには attribute 指定する。
  • 頂点データは四次元に拡張する。
  • highp と mediump は小数点の精度だが事実上関係なし。
  • 色は vec4 にて、 RGBA の順に構成される。

Perspective Transform

立体的に見えるように変換をしてみます。ビュー行列とプロジェクション行列を使います。

ビュー行列は LookAtLH 関数、プロジェクション行列は PerspectiveFovLH が分かりやすいのでコレを利用させて頂きましょう。 LH はLeftHand すなわち左手系を表します。
左手系では向かって右が X のプラス方向、 上が Y のプラス方向、 遠くが Z のプラス方向になります。さきほどの変換無しと比較して奥行き Z が加わっただけなので分かりやすいです。

まず LookAtLH の設定

Vector3 from,at,up;
from=vector3( 2.0f, 5.0f,-10.0f);
at=  vector3( 0.0f, 0.0f,  0.0f);
up=  vector3( 0.0f, 1.0f,  0.0);

Matrix view_;
LookAtLH(&view_,from,at,up);

from はカメラの位置、 at はカメラの目標になる位置、 up はカメラの上向きを設定します。寝転がったシーンや、揺れるシーンを表現するときに up の向きを変えます。
この値の場合、カメラの位置が先ほどの三角形に対して、ちょっと右、やや上、とても後ろ、という設定です。

つぎに PerspctiveFovLH

float near_clip,far_clip,fov,aspect;
near_clip=1.0f;
far_clip=100.0f;
fov=PI/4.0f;
aspect=9.0f/16.0f;

Matrix projection_;
PerspectiveFovLH(&projection_,fov,aspect,nearclip,farclip);

near/far がそれぞれ最前面と最後面です。 near/far の差が大きすぎるとポリゴンが重なったときの誤差により見えたり見えなかったりする面が出るので差は大きくないほうが良いでしょう。 fov が視野角になり、小さくなるほどズームします。だいたい 45 度前後を設定することが多いようです。 PI が 180 度なので /4 を設定します。 aspect が縦横比になります。

VertexShader コード

attribute highp vec3 in_position;
uniform mediump mat4 view_projection_matrix;
void main(void){
  highp vec4 position=vec4(in_position,1);
  position = position * view_projection_matrix;
  gl_Position=position;
}

position と matrix を掛ける順に注意してください。行列は掛ける順によって結果が変わってしまいます。 vector を matrix で掛けるほうがベクトルプロセッサである GPU と相性が良いようです。
uniform 指定することによって、頂点行列とは別にアプリケーションから定数を設定できるようになります。

uniform へ設定

Matrix view_projection_;
view_projection_=view_*projection_;
glUniformMatrix4fv(
glGetUniformLocation(po,"view_projection_matrix"),
1,GL_FALSE,MatrixTranspose(view_projection_));

vector を matrix で掛けるときは行列を転置 (transpose) するようにしてください。

これを実行すると、

となります。非常にわかりにくいですが、カメラを右後ろ上方から原点を見下ろすように設置し、視野 45 度の画面になります。

まとめ

  • カメラ行列には LookAtLH を使う。
  • プロジェクション行列には PerspectiveFovLH を使う。
  • near と far の差の指数は小さいほうが良い。
  • シェーダへ行列を設定するときは Transpose する。
  • ベクトルを変換するときは ベクトル*行列 にする。
  • 定数の設定には uniform 指定する。
  • まともなポリゴンデータが読み込めるまではガッカリなシーンしか構成できない。

World Transform

ワールド行列を使い、対象の位置や姿勢を変更させます。モデル行列なんて言い方もあります。パース変換を行わずに 2D 描画のアイコンなどを拡大縮小回転並行移動するのにも使います。

行列作成。行列を作るには、状態を変化させていくクラスと、それぞれの行列を単体で作り、掛け合わせて行く二つの方法があるようです。以下は状態を変化させていくクラスの例です。

Matrix world;
world.Identity();
world.Translate(0.0f, 0.0f, -4.0f);

Z 軸のマイナスは、手前なので、さきほどのカメラ設定の場合、手前側に三角形が表示されるのを期待します。

VertexShader コード

attribute highp vec3 in_position;
uniform mediump mat4 world_matrix;
uniform mediump mat4 view_projection_matrix;
void main(void){
  highp vec4 position=vec4(in_position,1);
  position = position * world_matrix;
  position = position * view_projection_matrix;
  gl_Position=position;
}

world_matrix にて変換する部分が増えています。 world_matrix のような三次元座標のみの変換には三行だけで良いのですが GLSL で 4×3 行列をスマートに扱えないのでこのようにしています。

glUniformMatrix4fv(
glGetUniformLocation(po,"world_matrix"),
1,GL_FALSE,MatrixTranspose(world));

転置して uniform に設定します。

実行すると・・・

三角形が手前へ。

world.Translate(0.0f, 0.0f, -4.0f); を world.Translate(0.0f, 0.0f, 14.0f); にすると・・・

三角形が奥へ。

どちらも分かりにくいですね。

三角形の Z 座標を -5 から +50 まで for で回して描画すると・・・

若干分かりやすくなりました。遠くのものほど小さいので遠近法が効いている証拠です。

このように World 変換を使うと物体の姿勢だけ変更し、複数描画することが楽になります。

全体のまとめ

  • World View Projection を使う。
  • World 行列を操作し、物体を動かす。
  • 変換の流れをしっかり把握する。
  • OpenGL は右手座標系だが、 GLSL になるとまったく関係なく、作る行列で決まる。
  • かける順、転置、 attirbute の設定場所など、順序が重要。