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次元座標で頂点を格納する際に発生する座標の重複の問題を解決する

  1. その立体図形が使うユニークな(重複しない)座標だけを集めた頂点バッファ(vertex buffer)を作る
  2. 三角形のそれぞれの頂点を指定するのに頂点バッファへのインデックスを使う(インデックスバッファ(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++側で読み込む必要がある

  1. 頂点シェーダーをロードしてコンパイル
  2. フラグメントシェーダーをロードしてコンパイル
  3. 2つのシェーダーをリンクしてシェーダープログラムにする
Shader.h

複数のステップでシェーダーを読み込む

メンバ変数がシェーダーオブジェクトIDに対応している(mVertexShadermFragShadermShaderProgram)(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をmVertexShadermFragShaderに保存する

コンパイルした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では、Matrix3Matrix4クラスを定義するほか、必要な機能をすべて実装するための演算子、メンバ関数、静的関数を定義している

行列の乗算

(省略)

行列で座標を変換する

座標が”“なのか、"“なのかによって、座標が乗算の"左側"になるのか、“右側"になるのかが決まる

線形代数の教科書では、たいがい列ベクトルが使われるが、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型指定子を使い、新しいグローバル変数を宣言する(この値はシェーダープログラムが何度実行されても値が変わらない)

ここでは、ワールド変換とビュー射影変換を行う行列のために宣言する(uWorldTransformuViewProj

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.vertSprite.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()の呼び出しでsrcFactordstFactorを指定する

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

まとめ

参考文献

Real Time Rendering Fourth Edition

Learn OpenGL

OpenGL Reference Pages