クロの制作日記

初心者が頑張るゲームプログラミングC++の練習問題5(シェーダーを用いたゲーム開発)

ゲームプログラミングC++

以下の本でSDLOpenGLを用いたC/C++ゲーム開発の勉強を進めています。


ゲームプログラミングC++

各章には練習問題があるので、その問題の私なりの解答コードと詰まった箇所をここに残しておきます。


環境構築編はこちら
SDL_imageとOpenGLというライブラリを使うので、それの環境構築がまだな人は以下の記事から構築をしてください。
kurora-shumpei.hatenablog.com

練習問題1はこちら
kurora-shumpei.hatenablog.com

練習問題2はこちら
kurora-shumpei.hatenablog.com

練習問題3はこちら
www.kuroshum.com

練習問題4はこちら
www.kuroshum.com




練習問題の内容

第5章ではGLSLを用いた基本的なシェーダーの使い方が主題となっています。

練習問題では,

  1. 背景色をスムーズに変化させる
  2. スプライトの頂点に頂点カラーを反映させ、テクスチャの色と頂点カラーの平均色を描画する

となっています。

シェーダーを少しでも触ったことがある人ならそこまで難しくないのかもしれませんが、私は今ままでシェーダから逃げていた人生を送っていたので大分苦戦しました。もう二度と触りたくねえなあと思いながら練習問題を実装していましたが、実装を終えると何となくシェーダの構造を理解できて楽しくなっちゃたので、また機会があれば色々やっていこうと思います。

練習問題の解答

解答のコードは以下のGitHubリポジトリのChapter5に置いています。
Star押してくれたらありがたいです。
github.com

ただ、解答のコードはC/C++初心者の私が作成したもので、これが正解だ!というわけではありませんし、他にもっと簡単かつ上手い実装方法があるということを念頭に置いた上で上記の解答コードを参考にしてくれたらありがたいです。

f:id:kurora-shumpei:20200404235019g:plain
リザルト画面

個人的に詰まったとこ

memset

memcpyは知ってたけどこれは知らんかった。
ja.cppreference.com

シェーダー

今まではできるだけ避けてきたシェーダーですが、ついに向き合わなければならない時が来てしまいました。とりあえず付け焼刃ですが本やネットで調べた知識をまとめておきます。

参考にしたサイトです。ぶっちゃけこのサイト見る方だけで理解はできます。この下に書くのは私が理解するために個人的にまとめたものですので、興味があればみてください。
marina.sys.wakayama-u.ac.jp
o-healer.hatenablog.com

シェーダーの役割

シェーダーの役割を一言で表すと、頂点の情報を元にポリゴンをいい感じに描画するものです。例えば、青色の三角形のポリゴンを描画することを考えると以下のようななステップになります。

  1. まず描画したい三角形の頂点の座標を設定し、それぞれの頂点を描画(頂点シェーダ)。(頂点の数だけ呼び出され実行する)
  2. その頂点をもとに、どのピクセルに青色を塗ればいいかを計算(ラスタライズ)する。
  3. 三角形の形に対応するピクセルに青色を塗る(フラグメントシェーダー)。

この3ステップを行うのがシェーダーとなります。

また、シェーダーも一枚岩ではなく、頂点シェーダとフラグメントシェーダーという2種類のシェーダーがあります。それぞれのシェーダーの役割を説明すると長くなるので、↑の大雑把な説明で勘弁してください。詳しく知りたい人は上に貼ったサイトを参考に...。

OpenGLとGLSL

シェーダをいじるためにはC/C++とは別にGLSLというシェーダ専用の言語を使用します。GLSL自体はC言語を元に作られた言語らしいので、記述するときに目新しさはあまりないので、とっつきやすいとは思います。

ただ、中々面倒なのはC/C++で記述したファイルで宣言したデータ(頂点座標など)をGLSLで記述したファイルに渡す必要があるのですが、言語が違うので直接渡すことができないので、OpenGLの関数を経由させる必要があります。また、頂点(バーテックス)シェーダからフラグメントシェーダーにもデータを渡す場合もあります、

f:id:kurora-shumpei:20200404175112p:plain
GLSLにおけるデータの受け渡し

上の図は大分簡略化しているので、厳密にはCPUのスレッドからGPUに~みたいな内部で色々やっていると思います。取り敢えず私はそんな感じのイメージで理解しているので、間違えているとか厳密にはこうだぞ!っていうのがあれば教えてください。

inとoutとuniform

データの受け渡しには、in・out・uniformといった修飾子(publicやprivateみたいなもの)を付けた変数を用います。inはデータを受け取る用の変数、outはデータを出力するようの変数、uniformもinと同じようにデータを受け取る変数ですが、モデルを描画する際にシェーダプログラムが何度実行されても値が変わりません(inとoutはシェーダが実行されるたびに値が変化)。

ちなみに、inとoutはOpenGL3.X系での表記で、OpenGL2.X系ではattributeとvaryingと表記するようです。

使用例を以下に示します。

  • Sprite.vert
// GLSL 3.3を要求
#version 330

// ワールド座標変とビュー投影のuniform変数
uniform mat4 uWorldTransform;
uniform mat4 uViewProj;

// 属性0は位置、属性1はテクスチャ座標
layout(location = 0) in vec3 inPosition;
layout(location = 1) in vec2 inTexCoord;

// 出力に、テクスチャ座標を追加
out vec2 fragTexCoord;

void main()
{
    // 位置座標を同時座標系に変換
    vec4 pos = vec4(inPosition, 1.0);
    // 位置をワールド空間に、そしてクリップ空間に変換
    gl_Position = pos * uWorldTransform * uViewProj;
    // テクスチャ座標をフラグメントシェーダーに渡す
    fragTexCoord = inTexCoord;
}
  • Sprite.frag
// GLSL 3.3を要求
#version 330

// 頂点シェーダーからのテクスチャ座標入力
in vec2 fragTexCoord;

// 出力色
out vec4 outColor

// テクスチャサンプリング用
uniform sampler2D uTexture;

void main()
{
	// テクスチャから色をサンプリングする
	outColor = texture(uTexture, fragTexCoord);
}

頂点シェーダのSprite.vertでは、VertexArray.cppのコンストラクタやShader.cppのSetMatrixUniform関数などから渡された情報をinとuniform変数に代入し、テクスチャ座標をフラグメントシェーダに渡します。

フラグメントシェーダでは、そのテクスチャ座標とTexture.cppから渡されたテクスチャをinとuniform変数に代入し、最終的にテクスチャ(の色)を(OpenGL側に)出力します。

頂点シェーダでのin変数が複数ある場合は、layout(location = 〇)という風にデータに番号を振り分ける必要があります。一方で、フラグメントシェーダのin変数は、頂点シェーダのout変数と同じ名前にしておけば、番号を割り振らずに使用できます。uniform変数もC/C++側の記述で変数名を指定するので、番号を割り振らなくて大丈夫です。

シェーダープログラム完成までの流れ

シェーダを使うための設定の流れは以下のようになります。

  1. 頂点シェーダとフラグメントシェーダーを作成(glCreateShader)
  2. 頂点シェーダとフラグメントシェーダーをロードしてコンパイルする(glCompileShader)
  3. プログラムオブジェクトを作成する(glCreateProgram)
  4. プログラムオブジェクトにシェーダーを登録(アタッチ)する(glAttachShader)
  5. 頂点シェーダとフラグメントシェーダーをシェーダプログラムにリンクする(glLinkProgram)
  6. シェーダを適用する(glUseProgram)
頂点カラーの渡し方

練習問題2で頂点シェーダとフラグメントシェーダーを書き換えて頂点カラーを反映させましょうというのがまったくやり方がわからず、四苦八苦しておりました。

最初は、頂点座標やインデックス配列と同じように、頂点カラーの配列を用意して頂点シェーダに渡そうと、カラーバッファを作成してバインドして、glVertexAttribPointer関数使ってなんか色々やったのですが、全くうまくいかず...。

色々調べていたら、以下のサイトで頂点座標とテクスチャ座標を保存している配列に頂点カラーも一緒に記述されていたので、同じようにやったらうまくいきました。実は、それ以外にもテクスチャ座標とインデックス配列がごっちゃになっていたり、glVertexAttribPointer関数の仕様を勘違いしていたりしていたので、中々苦労しましたが、何とかそれっぽく動きました。
maku.blog

恐らくですが、頂点カラーを頂点座標とテクスチャ座標とは別の配列を宣言してもできるとは思うので、知っている人がいればご教授お願いします。




最後に

今回はChapter5の練習問題を扱いました。

シェーダーを初めて勉強しましたがやはり難しいですね。ただ、やっと低レイヤーを意識した実装を行った気がするのでちょっとわくわくしました。

卒論も学会発表も無事に終わり(学会発表はコロナの影響で無くなりましたが...)、4月は多少余裕があると思うので、次々に進めていきたいと思っています。

ただ、個人的に3Dは余り興味がないというか勉強する時間がないので、6章は飛ばして7章に入るかもしれませんがご容赦ください。

ではまた次回に...。