C++で作るゲームエンジン自作シリーズ
| Part1 設計編 | Part2 ウィンドウ | Part3 OpenGL | Part4 シェーダー | Part5 テクスチャ | Part6 3Dモデル | Part7 当たり判定 |
|---|---|---|---|---|---|---|
| - | - | - | 👈 Now | - | - | - |
はじめに
前回は固定色の三角形を描いた。
今回はシェーダーを使って:
- 色を動的に変える
- アニメーションさせる
- 変換行列を使う
シェーダーを書くと、GPUと対話している感覚になる。
GLSL基礎
GLSL(OpenGL Shading Language)の基本を押さえよう。
データ型
float x = 1.0; // 浮動小数点
int i = 1; // 整数
bool b = true; // 真偽値
vec2 v2 = vec2(1.0, 2.0); // 2次元ベクトル
vec3 v3 = vec3(1.0, 2.0, 3.0); // 3次元ベクトル
vec4 v4 = vec4(1.0, 2.0, 3.0, 1.0); // 4次元ベクトル
mat2 m2; // 2x2行列
mat3 m3; // 3x3行列
mat4 m4; // 4x4行列
スウィズル
ベクトルの要素を自由に取り出せる:
vec4 color = vec4(1.0, 0.5, 0.2, 1.0);
vec3 rgb = color.rgb; // (1.0, 0.5, 0.2)
vec3 bgr = color.bgr; // (0.2, 0.5, 1.0) 順番入れ替え
vec2 xy = color.xy; // (1.0, 0.5)
float r = color.r; // 1.0
// 同じ要素を繰り返すことも可能
vec3 rrr = color.rrr; // (1.0, 1.0, 1.0)
組み込み関数
sin(x), cos(x), tan(x) // 三角関数
pow(x, y) // x^y
sqrt(x) // 平方根
abs(x) // 絶対値
min(a, b), max(a, b) // 最小・最大
clamp(x, min, max) // 範囲制限
mix(a, b, t) // 線形補間 a*(1-t) + b*t
step(edge, x) // x < edge ? 0 : 1
smoothstep(e0, e1, x) // 滑らかなステップ関数
uniform変数
uniformは、C++からシェーダーにデータを送る仕組み。
シェーダー側
#version 330 core
uniform float time; // C++から送られてくる
uniform vec3 tintColor; // C++から送られてくる
void main() {
// timeやtintColorを使える
}
C++側
// シェーダープログラムを使用
glUseProgram(shaderProgram);
// uniform変数の位置を取得
int timeLoc = glGetUniformLocation(shaderProgram, "time");
int tintLoc = glGetUniformLocation(shaderProgram, "tintColor");
// 値を設定
glUniform1f(timeLoc, glfwGetTime());
glUniform3f(tintLoc, 1.0f, 0.5f, 0.2f);
glUniform の種類:
-
glUniform1f: float 1つ -
glUniform3f: float 3つ(vec3) -
glUniform4f: float 4つ(vec4) -
glUniformMatrix4fv: 4x4行列
アニメーションシェーダー
時間で色と位置を変化させるシェーダーを作ろう。
頂点シェーダー(animated.vert)
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aColor;
out vec3 vertexColor;
uniform float time;
uniform mat4 transform;
void main() {
// 時間で揺らす
vec3 pos = aPos;
pos.x += sin(time + aPos.y * 3.0) * 0.1;
pos.y += cos(time + aPos.x * 3.0) * 0.1;
gl_Position = transform * vec4(pos, 1.0);
vertexColor = aColor;
}
sin と cos で波のような動きを作る。
フラグメントシェーダー(animated.frag)
#version 330 core
in vec3 vertexColor;
out vec4 FragColor;
uniform float time;
uniform vec3 tintColor;
void main() {
// 時間で色を変化させる
vec3 color = vertexColor * (0.5 + 0.5 * sin(time));
color = mix(color, tintColor, 0.3);
FragColor = vec4(color, 1.0);
}
mix で元の色と tintColor を30%ブレンド。
変換行列
3Dグラフィックスでは行列で座標変換する。
変換の種類
Model行列: オブジェクトの位置・回転・スケール
View行列: カメラの位置・向き
Projection行列: 3D→2D変換(透視投影)
最終座標 = Projection × View × Model × 頂点座標
今回は2Dなので、単純な回転行列を使う:
// 回転行列を作成(GLMを使用)
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
float angle = glfwGetTime(); // 時間 = 角度
glm::mat4 transform = glm::mat4(1.0f); // 単位行列
transform = glm::rotate(transform, angle, glm::vec3(0.0f, 0.0f, 1.0f));
// シェーダーに送る
int transformLoc = glGetUniformLocation(shaderProgram, "transform");
glUniformMatrix4fv(transformLoc, 1, GL_FALSE, glm::value_ptr(transform));
これで三角形がくるくる回転する!
シェーダーのホットリロード
開発中はシェーダーを変更するたびにアプリを再起動したくない。
簡易的なホットリロード:
// ファイルの更新時刻をチェック
std::filesystem::file_time_type lastModified;
void checkShaderReload(unsigned int& shader) {
auto current = std::filesystem::last_write_time("shaders/animated.frag");
if (current != lastModified) {
lastModified = current;
// シェーダーを再コンパイル
shader = createShaderProgram("shaders/animated.vert", "shaders/animated.frag");
std::cout << "Shader reloaded!" << std::endl;
}
}
シェーダーのデバッグ
シェーダーのデバッグは難しい。printf が使えないから。
テクニック:
1. 色で値を可視化
// 法線を色として出力
FragColor = vec4(normal * 0.5 + 0.5, 1.0);
// UV座標を色として出力
FragColor = vec4(uv, 0.0, 1.0);
// 深度を色として出力
FragColor = vec4(vec3(gl_FragCoord.z), 1.0);
2. 範囲チェック
// 値が範囲外なら赤くする
if (value < 0.0 || value > 1.0) {
FragColor = vec4(1.0, 0.0, 0.0, 1.0); // 赤
return;
}
3. RenderDoc
GPUのデバッガー。フレームをキャプチャして、シェーダーの入出力を確認できる。
よくあるバグ
1. uniformが見つからない
int loc = glGetUniformLocation(program, "time");
// loc = -1 なら見つからなかった
原因:
- 変数名のスペルミス
- シェーダー内で使ってない変数は最適化で消される
2. 真っ黒になる
シェーダーのコンパイルエラーをチェック:
glGetShaderiv(shader, GL_COMPILE_STATUS, &success);
if (!success) {
char infoLog[512];
glGetShaderInfoLog(shader, 512, nullptr, infoLog);
std::cerr << infoLog << std::endl;
}
3. 変な色になる
色は0.0〜1.0の範囲。範囲外だとクランプされる。
// NG: 色が明るすぎ
FragColor = vec4(color * 2.0, 1.0);
// OK: clampする
FragColor = vec4(clamp(color, 0.0, 1.0), 1.0);
実行結果
三角形が:
- くるくる回転する
- 色が時間で変化する
- 形状が波打つ
シェーダーで表現の幅が一気に広がる!
まとめ
今回やったこと:
- GLSL基礎(型、スウィズル、関数)
- uniform変数でC++からデータを送る
- 時間ベースのアニメーション
- 変換行列で回転
シェーダーを書けるようになると、GPUを自在に操れる感覚になる。
次回はテクスチャを貼る!
次回予告
Part5: テクスチャ貼ったら急にゲームっぽくなった
画像をポリゴンに貼り付ける!