GameDev #5 OpenGL [Devlog #019]
Table of Contents
ゲーム開発者の教科書:Game Programming in C++ を読んで理解したことについてを要約します(内容の転載を避け、詳しく説明しすぎないように配慮します)
ゲームプログラミング in C++
OpenGL
ここでは、GameDev #3 ベクトルと物理法則 [Devlog #016]で作成したゲームプロジェクトの描画をOpenGLで書き換える(SDLのレンダラーは2Dグラフィックスのみをサポート)
OpenGLを初期化する
30年以上も使われているOpenGLは、2D/3Dグラフィックスのためのクロスプラットフォームな業界標準ライブラリである
ここではOpenGL 3.3までに定義された関数を使う
以下では、OpenGLとGLEW(ヘルパーライブラリ)の設定と初期化のステップを説明する
OpenGLウィンドウの設定
Game.cpp
Game::Initialize()
OpenGL用のウィンドウを作る前に、OpenGLのバージョンや色深度などの属性を設定する
SDL_GL_SetAttribute(
SDL_GLattr attr, // 設定する属性
int value // 属性の値
);
enum SDL_GLattr
にはさまざまな属性が列挙されている
OpenGLがサポートするメインプロファイルは、コア(core)、互換(compativility)、ESの三種類がある
OpenGLのレンダリングがグラフィクスハードウェア(GPU)を活用するハードウェアアクセラレーション付きの実行も指定できる
SDLでは、ウィンドウ作成時のSDL_CreateWindow
呼び出しの最後の引数に、SDL_WINDOW_OPENGL
フラグを渡すことで、作成されたウィンドウでOpenGLが使えるようになる
OpenGLコンテクストの初期化
Game.h
SDL_GLContext mContext
はOpenGLコンテクストのメンバ変数であり、OpenGLが関知するすべての要素を含む
Game.cpp
Game::Initialize()
SDL_GL_CreateContext()
でOpenGLコンテクストを作成する
Game::Shutdown()
SDL_GL_DeleteContext()
でOpenGLコンテクストもデストラクタで削除する
GLEWの初期化
後方互換性をサポートする拡張システムを自動的に初期化するGLEW(OpenGL Extension Wrangler Library)というオープンソースライブラリを利用する
今回は、OpenGL 3.3とそれ以前のバージョンが対応する全拡張機能を初期化する
Game.cpp
Game::Initialize()
glewExperimental = GL_TRUE
によって、一部のプラットフォームでコアコンテクストを使う時に発生する初期化エラーを予防する
glewInit()
でGLEWを初期化する
一部のプラットフォームでは、初期化時に無害のエラーコードがでるので、glGetError()
を呼び出してクリアする
フレームのレンダリング
Game.cpp
Game::GenerateOutput()
バッファをクリアし、シーンを描画してからバッファを交換するというプロセスを、OpenGL関数で処理する
引数にGL_COLOR_BUFFER_BIT
を設定してglClear()
を呼び出すと、指定した色でカラーバッファがクリアされる
SDL_GL_SwapWindow()
の呼び出しで、フロントバッファとバックバッファを交換する
三角形の基礎
ファミリーコンピューターのようなスプライトベースのゲーム機では、ブリッティング(スプライトの画像をカラーバッファ上の好きな場所にコピーするだけで表示できる手法)が効率的だったが、現在のグラフィクスハードウェアでは、ブリッティングは非効率である(結局はグラフィクス処理にポリゴンを利用している)
なぜポリゴンなのか
ポリゴンはスケーラブルであり、さまざまなスペックのハードウェアでも問題なく計算を行うことができる
三角形ポリゴンの利点として以下が挙げられる
- 頂点が3つでシンプル
- 三角形の3つの点は、必ず同一平面上にある(coplanar)
- 三角形は細分割(tessellate)が容易である
正規化デバイス座標系(NDC)
一般に、座標空間(coordinate space)は、原点がどこにあり、どの向きに座標値が増えるかを指定する
座標空間の基底ベクトル(basis vector)は、それらの座標値が増加する向きを示す
OpenGLのデフォルト座標系は正規化デバイス座標系(normalized device coordinate)である
OpenGLウィンドウにおいては中央が原点になり、左下隅は(-1,-1)、右上隅は(+1,+1)になる
正規化デバイス座標系のz成分は[-1,+1]の範囲で、正の方向が画面の奥に向かう方向である
頂点バッファとインデックスバッファ
3次元座標で頂点を格納する際に発生する座標の重複の問題を解決する
- その立体図形が使うユニークな(重複しない)座標だけを集めた頂点バッファ(vertex buffer)を作る
- 三角形のそれぞれの頂点を指定するのに頂点バッファへのインデックスを使う(インデックスバッファ(index buffer)に1つの三角形につき3個のインデックスを格納する)
float vertexBuffer[] = {
-0.5f, 0.5f, 0.0f, // 頂点 0
0.5f, 0.5f, 0.0f, // 頂点 1
0.5f, -0.5f, 0.0f, // 頂点 2
-0.5f, -0.5f, 0.0f, // 頂点 3
};
unsigned short indexBuffer[] = {
0, 1, 2,
2, 3, 0
};
頂点バッファとインデックスバッファは、その存在をOpenGLに知らせなければ使えない
OpenGLは、頂点配列オブジェクト(vertex array object)を使って1個の頂点バッファと1個のインデックスバッファと頂点レイアウトをカプセル化する
頂点レイアウト(vertex layout)は、モデルの各頂点のデータ形式を指定する(今回は3次元の位置座標)
VertexArray.h
どのモデルも頂点配列オブジェクトを使うので、モデルをVertexArray
クラスでカプセル化する
コンストラクタは、頂点バッファおよびインデックスバッファの配列ポインタを受け取り、データをOpenGLに渡す
OpenGLは作成したオブジェクトへのポインタを返さないため、整数のID番号を受け取る
VertexArray.cpp
VertexArray::VertexArray()
コンストラクタでは、まずglGenVertexArrays()
で頂点配列オブジェクトを作成し、glBindVertexArray()
で結合し、そのIDをメンバ変数mVertexArray
に保存する
glGenBuffers()
で頂点バッファを作成し、glBindBuffer()
で結合する
GL_ARRAY_BUFFER
は頂点バッファに使うという意味
頂点バッファができたら、glBufferData()
でコンストラクタに渡された頂点データverts
をこの頂点バッファにコピーする
glBufferData(
GL_ARRAY_BUFFER, // バッファの種類
numVerts * 3 * sizeof(float), // コピーするバイト数
verts, // コピー元(ポインタ)
GL_STATIC_DRAW // このデータの利用方法
);
GL_STATIC_DRAW
は、GPUでデータのロードを1回だけ行い、その後、描画で頻繁にデータを読み込む際に使う
次にglGenBuffers()
でインデックスバッファを作成し、glBindBuffer()
で結合する
GL_ELEMENT_ARRAY_BUFFER
はインデックスバッファに使うという意味
glBufferData()
でインデックスデータindices
をインデックスバッファにコピーする
最後に、glEnableVertexAttribArray()
で頂点レイアウト(頂点属性(vertex attributes))を指定する
glVertexAttribPointer()
で属性のサイズ、種類、フォーマットを指定する
glVertexAttribPointer(
0, // 属性インデックス
3, // 要素数
GL_FLOAT, // 要素の型
GL_FALSE, // (整数型のみ使用する)
sizeof(float) * 3, // ストライド(通常は各頂点のサイズ)
0 // 頂点データの開始位置からこの属性までのオフセット
);
ストライド(歩幅)は、連続する頂点属性間のバイトオフセットであり、頂点バッファに頂点データ間のパディング(隙間)がない時は単に頂点のサイズになる
VertexArray::~VertexArray()
デストラクタでは、頂点バッファとインデックスバッファと頂点配列オブジェクトを破棄する
VertexArray::SetActive()
利用する頂点配列を指定するglBindVertexArray()
を呼び出す
Game.cpp
Game::CreateSpriteVerts()
VertexArray
のインスタンスを作り、Game
のメンバ変数mSpriteVerts
に保存する(スプライト描画で使う)
mSpriteVerts = new VertexArray(vertices, 4, indices, 6);
頂点バッファに4つの頂点があり、インデックスバッファに6個のインデックスがある(四角形を作る2つの三角形)
Game::Initialize()
CreateSpriteVerts()
で頂点配列オブジェクトを作成する
シェーダー
現代のグラフィクスパイプラインでは、頂点とインデックスのバッファを与えた上で、どのように描画するのかを指定する(固定色 or テクスチャの色?、ピクセルすべてにライティングの計算を行う? など)
OpenGLを含むグラフィクスAPIでは、グラフィクスハードウェアで実行されるシェーダープログラム(shader program)をサポートしている
シェーダーは別プログラムであり、独自のmain関数を持つ
頂点シェーダー
頂点シェーダー(vertex shader)は、描画されるすべての頂点について1回ずつ実行される
頂点属性データを入力として受け取り、それらの頂点属性を必要に応じて変更する
頂点バッファとインデックスバッファを使うと、三角形が頂点を共有しているため、頂点シェダーの呼び出し回数は減少する
フラグメントシェーダー
三角形に対応するカラーバッファの画素を決定するのがラスタライズである
フラグメントシェーダーまたはピクセルシェーダーは、それぞれの画素につき少なくとも1回は実行される
色には、モデル表面の属性(テクスチャ、色、材質など)が反映され、必要によって照明計算が行われる
基礎的なシェーダーを書く
Basic.vert(頂点シェーダー)
GLSLは、C/C++と違い、main関数が引数を取らない
シェーダーの入力はグローバル変数のような形をとり、特別なinキーワードが付く
GLSLは、シェーダー出力にもグローバル変数を使う(今回はシェーダーの頂点位置が出力であり、組み込み変数gl_Position
に格納する)
inPosition
変数の型はvec3
で、3個の浮動小数点数のベクトルに対応する(この中に、頂点の位置に対応するx、y、zが入る)
#version 330 // GLSLプログラミング言語のバージョンの指定
in vec3 inPosition; // それぞれの頂点の頂点属性を指定
void main() {
gl_Position = vec4(inPosition, 1.0);
}
Basic.frag(フラグメントシェーダー)
青い色をすべてのピクセルにハードコーディングで出力する
#version 330
out vec4 outColor
void main() {
outColor = vec4(0.0, 0.0, 1.0, 1.0);
}
シェーダーを読み込む
OpenGLにシェーダーの中身を知らせるために、各ファイルをC++側で読み込む必要がある
- 頂点シェーダーをロードしてコンパイル
- フラグメントシェーダーをロードしてコンパイル
- 2つのシェーダーをリンクしてシェーダープログラムにする
Shader.h
複数のステップでシェーダーを読み込む
メンバ変数がシェーダーオブジェクトIDに対応している(mVertexShader
、mFragShader
、mShaderProgram
)(GLuint
はunsigned intのOpenGL版)
CompileShader()
、IsCompiled()
、IsValidProgram()
は、Load()
から使えるヘルパー関数である(privateセクションでの宣言)
Shader.cpp
Shader::CompileShader()
コンパイルするシェーダーファイルの名前、シェーダーの種類、シェーダーのIDを格納する参照変数の3つの引数を受け取る
bool型の戻り値によって成功したかを示す
ファイルをロードするためにifstream
を作成し、文字列ストリームstringstream
を使って、ファイル全体の内容を文字列contents
にロードし、c_str
関数でC言語スタイルの文字列ポインタを作る
glCreateShader()
でOpenGLシェーダーオブジェクトを作成し、IDをoutShader
に保存する
glShaderSource()
でシェーダーソースコードの文字列を指定する
glCompileShader()
でコンパイルし、ヘルパー関数のIsCompiled()
で正常動作かどうかを確認する
Shader::IsCompiled()
シェーダーオブジェクトがコンパイルされたことを確認し、失敗ならばコンパイルエラーメッセージを出力する
glGetShaderiv()
は、コンパイルの状態を整数の状態コードで返す
glGetShaderInfoLog()
は、コンパイルエラーメッセージを出力する
Shader::Load()
頂点シェーダーとフラグメントシェーダーのファイル名を受け取って、2つのシェーダーのコンパイルとリンクを行う
CompileShader()
でコンパイルし、それぞれのオブジェクトIDをmVertexShader
とmFragShader
に保存する
コンパイルした2つをリンクして、シェーダープログラムというオブジェクトにまとめる(OpenGLは、アクティブなシェーダープログラムを使って三角形をレンダリングする)
glCreateProgram()
でシェーダープログラムを作成し、glAttachShader()
で頂点シェーダーとフラグメントシェーダーをシェーダープログラムに追加する
glLinkProgram()
でアタッチしたシェーダーのすべてをリンクし、ヘルパー関数のIsValidProgram()
で正常動作かどうかを確認する
Shader::IsValidProgram()
リンクが成功したかどうかを確認し、失敗ならばエラーメッセージを出力する
glGetProgramiv()
は、シェーダープログラムの状態を整数の状態コードで返す
glGetProgramInfoLog()
は、エラーメッセージを出力する
Shader::SetActive()
glUseProgram()
でシェーダープログラムをアクティブにする
Shader::Unload()
glDeleteProgram()
でシェーダープログラム、glDeleteShader()
で頂点シェーダー、フラグメントシェーダーを削除する
Game.h
Shader
のポインタをGame
のメンバ変数に追加する
最終的にスプライトの描画に使う
class Shader* mSpriteShader;
Game.cpp
Game::LoadShaders()
2つのシェーダーファイルをロードして、シェーダープログラムをアクティブにする
Game::Initialize()
LoadShaders()
の呼び出しは、OpenGLとGLEWの初期化を終えた直後に行う(頂点配列オブジェクトmSpriteVerts
を作成する前)
三角形を描画する
スプライトを描画する
SpriteComponent.h
SDL_Renderer*
の代わりにShader*
を引数で受け取るように変更
SpriteComponent::Draw()
glDrawElements()
で四角形を描画する
glDrawElements(
GL_TRIANGLES, // 描画するポリゴン/プリミティブの種類
6, // インデックスバッファにあるインデックス数
GL_UNSIGNED_INT, // インデックスの型
nullptr // 通常はnullptr
);
Game::GenerateOutput()
このglDrawElements()
の呼び出しには、アクティブな頂点配列オブジェクトとアクティブなシェーダーが必要である
~
mSpriteShader->SetActive();
mSpriteVerts->SetActive();
for (auto sprite : mSprites) {
sprite->Draw(mSpriteShader);
}
~
座標変換の基礎
画面上の小惑星を別々の位置に出したいとしたとき、10個の小惑星のために10個の頂点バッファを作成して計算を行うのは効率が悪い
そこでスプライトを抽象化し、短形用に1つの頂点バッファを作り、それを再利用して描画する
NDCの単位正方形があれば、変換によって任意の位置、スケール、向きを持つ任意の短形を描画できる
オブジェクト空間
オブジェクト自身に相対的な座標系が、オブジェクト空間(object space)あるいはモデル空間(model space)である
ゲーム実行時には、個々のモデルを専用の頂点配列オブジェクト(VAO:vertex array object)にロードする
ワールド空間
ゲームワールドそのもののための座標空間は、ワールド空間(world space)である
ゲーム内のオブジェクトは、ワールド空間の原点からの相対的な位置、スケール、向きを持つ
ワールド空間への座標変換
目標は、“オブジェクト空間で自分の原点を中心にしている単位正方形"を、ワールド空間の原点に対する相対的な任意の位置に、任意のスケールや回転角を持つ短形として表現すること
例えば、短形のインスタンスの1つを、サイズを2倍、ワールド空間の原点から50単位右の位置に表示させるとしたとき、短形の頂点のそれぞれに対して演算を行う
平行移動
(省略)
スケーリング
(省略)
回転
(省略)
変換を組み合わせる
3つの変換を正しい順序で組み合わせる
座標変換の順序は結果に影響を与えるため、一貫した順序を保つことが重要である
オブジェクト空間からワールド空間への変換では、常にスケーリング・回転・平行移動の順序で適用する
連立方程式による方法の問題点
さまざまな変換を行列で記述し、行列の乗算を用いることで容易に変換を組み合わせることができる(2つの座標空間で基底ベクトルが異なる場合にも対応できる)
行列と変換
当プロジェクト独自のヘッダーファイルMath.h
では、Matrix3
とMatrix4
クラスを定義するほか、必要な機能をすべて実装するための演算子、メンバ関数、静的関数を定義している
行列の乗算
(省略)
行列で座標を変換する
座標が”行“なのか、"列“なのかによって、座標が乗算の"左側"になるのか、“右側"になるのかが決まる
線形代数の教科書では、たいがい列ベクトルが使われるが、CG分野ではリソースとグラフィクスAPIによって行ベクトルと列ベクトルを使い分ける
当プロジェクトでは行ベクトルを使う(座標の変換が"左から右"の順序で適用される)
行ベクトルの座標$q$を行列$T$で変換し、続いて行列$R$で変換する
$$ q^{\prime} = qTR $$
$q$を列ベクトルに切り替えると、以下の計算になる
$$ q^{\prime} = R^{T}T^{T}q $$
ワールド空間への座標変換(再び)
スケール行列
(省略)
回転行列
(省略)
平行移動行列
平行移動行列(taranslation matrix)を包括的に表現するため、同次座標系(homogenous coordinates)を利用し、n次元の空間をn+1個の成分によって表現する
同次座標の特殊な成分はw成分(w conponent)となり、2次元の座標では$(x, y, w)$、3次元の座標では$(x, y, z, w)$と表現する
変換を組み合わせる
オブジェクト空間からワールド空間への座標変換を行う行列が、ワールド行列(world transform matrix)である
2次元空間の場合: $$ ワールド行列 = S(s_x, s_y)R(θ)T(a,b) $$ $$ =\begin{bmatrix} s_x & 0 & 0 \\\ 0 & s_y & 0 \\\ 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} cos(θ) & sin(θ) & 0 \\\ -sin(θ) & cos(θ) & 0 \\\ 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} 1 & 0 & 0 \\\ 0 & 1 & 0 \\\ a & b & 1 \end{bmatrix} $$
このワールド行列は頂点シェーダーに渡すことができる
Actorにワールド変換を加える
位置のVector2
、スケールのfloat
、回転角のfloat
を組み合わせてワールド行列を作る
Actor.h
Matrix4
型のメンバ変数mWorldTransform
は、ワールド行列を格納する
bool
型のメンバ変数mRecomputeWorldTransform
は、ワールド行列の再計算の必要性を管理する(ワールド変換を計算するのはアクターの位置orスケールor回転角が変化したときだけ)
Actor.cpp
Actor::ComputeWorldTransform()
Matrix4
の静的関数を使って、個々の変換行列を作る
Matrix4::CreateScale()
は一様スケールの行列を作る
Matrix4::CreateRotationZ()
はz軸周りの回転行列を作る
Matrix4::CreateTranslation()
は平行移動行列を作る
Actor::Update()
ComputeWorldTransform()
は、UpdateComponents()
の前とUpdateActor()
の後に呼び出す(その間で変化した場合に備えて)
Game::UpdateGame()
ペンディング状態のアクターでも、作成されたフレームでワールド変換が計算されるようにする
Component.h
所有アクターのワールド変換が更新されたら通知されるようにする
virtual void OnUpdateWorldTransform()
Actor.cpp
Actor::ComputeWorldTransform()
各コンポーネントのOnUpdateWorldTransform()
を呼び出す
ワールド空間からクリップ空間への変換
ビュー射影行列(view-projection matrix)を用いて、頂点をワールド空間からクリップ空間(clip space)に変換する
正規化デバイス座標とは違い、クリップ空間にはw成分がある
ビュー行列は、ゲーム内のカメラがゲームワールドをどのように見るかを指定する
射影行列は、カメラのビューをクリップ空間に変換する方法を指定する
2Dグラフィクスの場合、単純なビュー射影行列を使う
任意の頂点$v$は、以下のようにオブジェクト空間からクリップ空間へと変換できる $$ v^{\prime} = v(ワールド行列)(単純なビュー射影行列) $$
解像度が1024×768で、ワールド空間の座標が$(256, 192)$だった場合: $$ \begin{bmatrix} 256 & 192 & 1 \end{bmatrix} \begin{bmatrix} 2/1024 & 0 & 0 \\\ 0 & 2/768 & 0 \\\ 0 & 0 & 1 \end{bmatrix} =\begin{bmatrix} 512/1024 & 384/768 & 1 \end{bmatrix} =\begin{bmatrix} 0.5 & 0.5 & 1 \end{bmatrix} $$
変換行列を使うようにシェーダーを更新する
Transform.vert
uniform
型指定子を使い、新しいグローバル変数を宣言する(この値はシェーダープログラムが何度実行されても値が変わらない)
ここでは、ワールド変換とビュー射影変換を行う行列のために宣言する(uWorldTransform
、uViewProj
)
main関数では、まず3次元のinPosition
を同時座標系へと変換する
vec4 pos = vec4(inPosition, 1.0);
そのオブジェクト空間の位置にワールド行列を掛け、さらにビュー射影行列を掛けることでクリップ空間の位置に変換する
gl_Position = pos * uWorldTransform * uViewProj;
Game.cpp
Game::LoadShaders()
頂点シェーダーに使うシェーダーをロードする
Shader.cpp
Shader::SetMatrixUniform()
uniform
変数をC++のコードから設定する
行列の他に、シェーダーファイル内の変数(文字列リテラルの名前)を引数とする
glGetUniformLocation()
で、uniform
のロケーションIDを取得する
glUniformMatrix4fv()
で、行列をuniform
に代入する
glUniformMatrix4fv(
loc, // Uniform ID
1, // 行列の数
GL_TRUE, // 行ベクトルを使う場合はTRUE
matrix.GetAsFloatPtr // 行列データへのポインタ
)
uniformバッファオブジェクト(UBO)は、シェーダー内の複数のuniformをグループ化して1つにまとめ、すべての変数を同時に送る(効率が良い)(フレームごとに更新するuniformやオブジェクトごとに更新するuniformなどに分けることができる)
このプロジェクトにおいて、
単純なビュー射影行列は、実行中に変化しないので設定は1回だけ、
ワールド行列は、描画すべき個々のスプライトコンポーネントごとに設定する
Game.cpp
Game::LoadShaders()
画面が1024×768という前提で、単純なビュー射影行列を作成して設定する
Matrix4 viewProj = Matrix4::CreateSimpleViewProj(1024.f, 768.f);
mSpriteShader->SetMatrixUniform("uViewProj", viewProj);
SpriteComponent.cpp
SpriteComponent::Draw()
以下の流れでSpriteComponentのためのワールド行列を求める
テクスチャの幅と高さによるスケーリングを行うためのスケール行列scaleMat
を作る
スケール行列と、所有者であるアクターのワールド行列mOwner->GetWorldTransform()
とを掛け合わせてこのスプライトのワールド行列world
を作る
SetMatrixUniform()
で頂点シェーダープログラムのuWorldTransform
を設定する
glDrawElements()
で三角形描画する
テクスチャマッピング
テクスチャマッピングでは、テクスチャを三角形の表面に貼ってレンダリングする
それぞれの頂点について、対応するテクスチャ内の位置を指定するテクスチャ座標(texture coordinate)が必要になる(UV座標とも呼ばれる)
例えば、キャラクターの顔にテクスチャを正しく貼り付けるには、テクスチャのどの部分がどの三角形に対応するのかを指定する
OpenGLの場合、テクスチャの左下隅が$(0, 0)$で、右上隅が$(1, 1)$になる
画像のピクセルデータの格納方法の違い(一般的な画像フォーマットはデータを上から格納する)に対処するため、以下の解決策がある
- V成分を逆転する
- 画像を上下逆にロードする
- ディスクに画像を上下逆に保存する
このプロジェクトではV成分を逆転する方法を用いる(DirectXのテクスチャ座標系に対応)
三角形のそれぞれの頂点が個別のUV座標を持つ
ピクセルを塗りつぶす際は、3つの頂点からの距離をベースとしたテクスチャ座標のブレンディング、または補間(interpolating)を行う
テクスチャ座標(UV座標)が対応するテクスチャの画素は、テクスチャピクセル(texture pixel)またはテクセル(texel)と呼ばれ、グラフィクスハードウェアがサンプリングする
最近傍フィルタリング(nearest-neighbor filtering)は、UV座標に最も近いテクセルの色を選択する方法である
双線形フィルタリング(bilinear filtering)は、複数の最近傍テクセルを合成した色を選択する方法である
OpenGLでテクスチャマッピングを行うには以下のステップが必要になる
- 画像ファイルをロードして、OpenGLのテクスチャオブジェクトを作る
- テクスチャ座標(UV座標)を含むように、頂点のフォーマットを更新する
- テクスチャを使うように、シェーダーを更新する
テクスチャをロードする
Texture.h
Simple OpenGL Image Library(SOIL)を使って、OpenGL用に画像をロードする
メンバ変数には、テクスチャの幅mWidth
と高さmHeight
、OpenGLのテクスチャIDmTextureID
を用意する
Texture.cpp
Texture::Load()
最初にチャンネル数を格納するローカル変数を宣言し、SOIL_load_image()
でテクスチャを読み込む
int channels = 0;
unsigned char* image = SOIL_load_image(
fileName.c_str(), // ファイル名
&mWidth, // 幅を記録
&mHeight, // 高さを記録
&channels, // チャンネル数を記録
SOIL_LOAD_AUTO // 画像ファイルの種類
);
if (image == nullptr)
{
SDL_Log("SOIL failed to load image %s: %s", fileName.c_str(), SOIL_last_result());
return false;
}
チャンネル数に応じてフォーマットを設定する(RGBならGL_RGB
、RGBAならGL_RGBA
)
glGenTextures()
で、OpenGLテクスチャオブジェクトを作成して、そのIDをmTextureID
に保存する
glBindTexture()
で、テクスチャをアクティブにする(GL_TEXTURE_2D
はテクスチャターゲットを表す)
glTexImage2D()
で、画像データをコピーする
glTexImage2D(
GL_TEXTURE_2D, // テクスチャターゲット
0, // 詳細レベル
format, // OpenGLが使うべきカラーフォーマット
mWidth, // テクスチャの幅
mHeight, // テクスチャの高さ
0, // 境界色
format, // 入力データのカラーフォーマット
GL_UNSIGNED_BYTE, // 入力データのビット深度
image // 画像データへのポインタ
);
SOIL_free_image_data()
で、SOILの画像データはメモリから解放される
glTexParameteri()
で、バイリニアフィルタを有効にする
Texture::Unload()
テクスチャオブジェクトを削除する
Texture::SetActive()
glBindTexture()
を呼び出す
Game.cpp
Game::GetTexture()
要求されたテクスチャfileName
を指すTexture*
を返す
SpriteComponent.h
SDL_Texture*
の代わりに、Texture*
のメンバ変数を用意する
SpriteComponent.cpp
SpriteComponent::Draw()
glDrawElements()
で頂点を描画する直前に、mTexture->SetActive()
を行うことで、描画するスプライトコンポーネントごとに異なるテクスチャを設定できる
頂点フォーマットを更新する
スプライトのVertexArray
を変更する
float vertices[] = {
-0.5f, 0.5f, 0.f, 0.f, 0.f // 左上
0.5f, 0.5f, 0.f, 1.f, 0.f, // 右上
0.5f, -0.5f, 0.f, 1.f, 1.f, // 右下
-0.5f, -0.5f, 0.f, 0.f, 1.f, // 左下
};
各頂点で、最初の3個の浮動小数点値が位置座標であり、続く2つの浮動小数点値がテクスチャ座標になる
VertexArray.cpp
VertexArray::VertexArray()
glBufferData()
で、コンストラクタに渡された頂点データverts
を頂点バッファにコピーする
glBufferData(
GL_ARRAY_BUFFER, // バッファの種類
numVerts * 5 * sizeof(float), // コピーするバイト数(3から5に変更)
verts, // コピー元(ポインタ)
GL_STATIC_DRAW // このデータの利用方法
);
頂点のストライドがfloat3個分から5個分に増えたので、glEnableVertexAttribArray()
での頂点レイアウト(頂点属性(vertex attributes))“0”を調整する
glEnableVertexAttribArray(0);
glVertexAttribPointer(
0, // 属性インデックス
3, // 要素数
GL_FLOAT, // 要素の型
GL_FALSE, // (整数型のみ使用する)
sizeof(float) * 5, // ストライド(通常は各頂点のサイズ)(※変更箇所)
0 // 頂点データの開始位置からこの属性までのオフセット
);
テクスチャ座標のために第2の頂点属性を追加したので、頂点レイアウト(頂点属性(vertex attributes))“1”を有効にしてフォーマとを指定する
glEnableVertexAttribArray(1);
glVertexAttribPointer(
1,
2,
GL_FLOAT,
GL_FALSE,
sizeof(float) * 5,
reinterpret_cast<void*>(sizeof(float) * 3) // 頂点データの開始位置からこの属性までのオフセット
);
OpenGLは最後の引数をオフセットポインタとして受け取るため、reinterpret_cast<>()
を使ってvoid*
ポインタ型へ強制的に変換する必要がある
シェーダーを更新する
Sprite.vert
複数の頂点属性に対して、どの属性スロットがどの変数に対応するのかをlayout
ディレクティブによって指定する
これらは、glVertexAttribPointer()
呼び出しのスロット番号に対応する
layout(location = 0) in vec3 inPosition;
layout(location = 1) in vec2 inTexCoord;
フラグメントシェーダーにテクスチャ座標を知らせるため、グローバルなout変数を頂点シェーダーの中に宣言して、main関数でコピーする
out vec2 fragTexCoord;
void main(){
~
fragTexCoord = inTexCoord;
~
}
OpenGLは、頂点シェーダーの出力を三角形ポリゴンの表面すべてに自動的に補間する
Sprite.frag
頂点シェーダーのout変数は、どれもフラグメントシェーダーのin変数に対応させなければならない(変数は名前も型も同じにする)
in vec2 fragTexCoord;
テクスチャサンプラーのためのuniformを追加する
sampler2D
型は、2Dテクスチャをサンプリングする型である
uniform sampler2D uTexture;
outColor
への代入を行う
頂点シェーダーから三角形の表面全体に補間されたテクスチャ座標を受け取り、テクスチャの色をサンプリングする
void main() {
outColor = texture(uTexture, fragTexCoord);
}
Game.cpp
Game::LoadShaders()
Load()
の引数を、Sprite.vert
とSprite.frag
に変更する
アルファブレンディング
アルファブレンディングは、半透明なピクセルを合成する処理である
フラグメントシェーダーから出力された色がソースカラーであり、その時点までカラーバッファに記録されていた色をデイスティネーションカラーになる
$$ outputColor = srcFactor \cdot srcColor + dstFactor \cdot dstColor $$
アルファブレンディングで半透明にするには、以下のようにsrcAlpha
を使う
$$ outputColor = srcAlpha \cdot srcColor + (1 - srcAlpha) \cdot dstColor $$
例えば、現状はsrcColor
が無色であり、dstColor
が黒色であるため、透明にすべきピクセルが黒塗りになっている
Game.cpp
Game::GenerateOutput()
スプライトを描画する直前にglEnable()
の呼び出しで、カラーバッファのブレンディングを有効にする
そしてglBlendFunc()
の呼び出しでsrcFactor
とdstFactor
を指定する
glEnable(GL_BLEND);
glBlendFunc(
GL_SRC_ALPHA, // srcFactorはsrcAplha
GL_ONE_MINUS_SRC_ALPHA // dstFactorは(1 - srcAplha)
);
ゲームプロジェクト
このプロジェクトは最新のドライバかOpenGLだとうまく動作しない可能性がある
問題のissue:https://github.com/gameprogcpp/code/issues/31