11
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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;
}

sincos波のような動きを作る。

フラグメントシェーダー(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);

実行結果

三角形が:

  • くるくる回転する
  • 色が時間で変化する
  • 形状が波打つ

シェーダーで表現の幅が一気に広がる

まとめ

今回やったこと:

  1. GLSL基礎(型、スウィズル、関数)
  2. uniform変数でC++からデータを送る
  3. 時間ベースのアニメーション
  4. 変換行列で回転

シェーダーを書けるようになると、GPUを自在に操れる感覚になる。

次回はテクスチャを貼る!

次回予告

Part5: テクスチャ貼ったら急にゲームっぽくなった

画像をポリゴンに貼り付ける!

11
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?