C++で作るゲームエンジン自作シリーズ
| Part1 設計編 | Part2 ウィンドウ | Part3 OpenGL | Part4 シェーダー | Part5 テクスチャ | Part6 3Dモデル | Part7 当たり判定 |
|---|---|---|---|---|---|---|
| - | - | 👈 Now | - | - | - | - |
はじめに
ウィンドウが開いたら、次は何かを描く!
3Dグラフィックスの世界では、すべてが三角形でできている。
┌─────────────────────────┐
│ △ │
│ △ △ │
│ △ △ │
│ △ △ │
│ △△△△△△ │ ← 四角形も三角形2つ
└─────────────────────────┘
だから最初にやるべきは三角形を描くこと。
三角形を描くのに必要なもの
- 頂点データ: 三角形の3つの頂点座標
- VBO: 頂点データをGPUに送る
- VAO: 頂点データの構造を定義
- シェーダー: GPUに描画方法を指示
順番に見ていこう。
頂点データ
三角形の3つの頂点を定義する。
float vertices[] = {
// 位置 // 色
-0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f, // 左下 (赤)
0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, // 右下 (緑)
0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f, // 上 (青)
};
座標系はNDC(Normalized Device Coordinates):
- X: -1.0(左端)~ +1.0(右端)
- Y: -1.0(下端)~ +1.0(上端)
- Z: -1.0(奥)~ +1.0(手前)
今回はZ=0の2D平面上に描く。
VBO(Vertex Buffer Object)
頂点データをGPUのメモリに転送する。
unsigned int VBO;
glGenBuffers(1, &VBO); // バッファを生成
glBindBuffer(GL_ARRAY_BUFFER, VBO); // バインド(これから操作する対象として指定)
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
GL_STATIC_DRAW は「データはほとんど変更されない」という意味。
他のオプション:
-
GL_DYNAMIC_DRAW: 頻繁に変更される -
GL_STREAM_DRAW: 毎フレーム変更される
VAO(Vertex Array Object)
VBOのデータ構造を定義する。「このデータの何番目から何番目が位置で、何番目から何番目が色」という情報。
unsigned int VAO;
glGenVertexArrays(1, &VAO);
glBindVertexArray(VAO);
// 位置属性 (location = 0)
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
// 色属性 (location = 1)
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3 * sizeof(float)));
glEnableVertexAttribArray(1);
glVertexAttribPointer の引数:
-
属性のインデックス: シェーダーの
location - 要素数: vec3なら3
- 型: GL_FLOAT
- 正規化: GL_FALSE
- ストライド: 次の頂点までのバイト数
- オフセット: この属性の開始位置
データ構造のイメージ:
vertices[] = [
位置X, 位置Y, 位置Z, 色R, 色G, 色B, ← 頂点0
位置X, 位置Y, 位置Z, 色R, 色G, 色B, ← 頂点1
位置X, 位置Y, 位置Z, 色R, 色G, 色B, ← 頂点2
]
↑ ↑
| |
offset=0 offset=12 (3*4bytes)
stride = 24 (6*4bytes)
シェーダー
シェーダーはGPU上で動くプログラム。**GLSL(OpenGL Shading Language)**で書く。
頂点シェーダー(basic.vert)
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aColor;
out vec3 vertexColor;
void main() {
gl_Position = vec4(aPos, 1.0);
vertexColor = aColor;
}
-
layout (location = 0) in vec3 aPos: VAOで設定した属性0を受け取る -
gl_Position: 出力。頂点の最終位置 -
out vec3 vertexColor: フラグメントシェーダーに渡す
フラグメントシェーダー(basic.frag)
#version 330 core
in vec3 vertexColor;
out vec4 FragColor;
void main() {
FragColor = vec4(vertexColor, 1.0);
}
-
in vec3 vertexColor: 頂点シェーダーから受け取る -
FragColor: 出力。ピクセルの色(RGBA)
なぜ色がグラデーションになる?
頂点ごとに違う色を指定すると、OpenGLが自動的に補間してくれる。
これをラスタライズという。
シェーダーのコンパイル
unsigned int compileShader(unsigned int type, const std::string& source) {
unsigned int shader = glCreateShader(type);
const char* src = source.c_str();
glShaderSource(shader, 1, &src, nullptr);
glCompileShader(shader);
// エラーチェック
int success;
glGetShaderiv(shader, GL_COMPILE_STATUS, &success);
if (!success) {
char infoLog[512];
glGetShaderInfoLog(shader, 512, nullptr, infoLog);
std::cerr << "Shader compilation failed:\n" << infoLog << std::endl;
}
return shader;
}
シェーダーのエラーチェックは必須。これがないとデバッグ不可能。
シェーダープログラムのリンク
unsigned int createShaderProgram(const std::string& vertPath, const std::string& fragPath) {
std::string vertSource = loadShaderSource(vertPath);
std::string fragSource = loadShaderSource(fragPath);
unsigned int vertShader = compileShader(GL_VERTEX_SHADER, vertSource);
unsigned int fragShader = compileShader(GL_FRAGMENT_SHADER, fragSource);
unsigned int program = glCreateProgram();
glAttachShader(program, vertShader);
glAttachShader(program, fragShader);
glLinkProgram(program);
// エラーチェック(省略)
glDeleteShader(vertShader);
glDeleteShader(fragShader);
return program;
}
頂点シェーダーとフラグメントシェーダーをリンクしてプログラムを作る。
描画
// メインループ内
glClearColor(0.1f, 0.1f, 0.2f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
glUseProgram(shaderProgram); // シェーダーを使用
glBindVertexArray(VAO); // VAOをバインド
glDrawArrays(GL_TRIANGLES, 0, 3); // 三角形を描画
glfwSwapBuffers(window);
glfwPollEvents();
glDrawArrays(GL_TRIANGLES, 0, 3):
-
GL_TRIANGLES: 三角形として描画 -
0: 開始インデックス -
3: 頂点数
実行結果
カラフルな三角形が表示される!
🔵
/ \
/ \
/ \
🔴------🟢
頂点ごとに色を指定したから、グラデーションになってる。
ハマりポイント
1. VAOをバインドし忘れ
// 描画時にVAOをバインドし忘れると何も表示されない
glBindVertexArray(VAO); // ← これ忘れがち
glDrawArrays(GL_TRIANGLES, 0, 3);
2. シェーダーのコンパイルエラー
GLSLは型に厳しい。floatとintを混ぜるとエラー。
// NG
gl_Position = vec4(aPos, 1); // 1はint
// OK
gl_Position = vec4(aPos, 1.0); // 1.0はfloat
3. 頂点の順番(巻き方向)
OpenGLはデフォルトで反時計回りの三角形を表面とみなす。
0
/|
/ |
/ |
2---1 反時計回り = 表面
時計回りだと裏面扱いになって、カリング(裏面除去)が有効だと表示されない。
リソースの解放
終了時にGPUリソースを解放:
glDeleteVertexArrays(1, &VAO);
glDeleteBuffers(1, &VBO);
glDeleteProgram(shaderProgram);
C++はガベージコレクションがないから、自分で解放する必要がある。
完全なコード
#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <iostream>
#include <fstream>
#include <sstream>
std::string loadShaderSource(const std::string& filepath) {
std::ifstream file(filepath);
std::stringstream buffer;
buffer << file.rdbuf();
return buffer.str();
}
unsigned int compileShader(unsigned int type, const std::string& source) {
unsigned int shader = glCreateShader(type);
const char* src = source.c_str();
glShaderSource(shader, 1, &src, nullptr);
glCompileShader(shader);
int success;
glGetShaderiv(shader, GL_COMPILE_STATUS, &success);
if (!success) {
char infoLog[512];
glGetShaderInfoLog(shader, 512, nullptr, infoLog);
std::cerr << "Shader error: " << infoLog << std::endl;
}
return shader;
}
unsigned int createShaderProgram(const std::string& vertPath, const std::string& fragPath) {
unsigned int vert = compileShader(GL_VERTEX_SHADER, loadShaderSource(vertPath));
unsigned int frag = compileShader(GL_FRAGMENT_SHADER, loadShaderSource(fragPath));
unsigned int program = glCreateProgram();
glAttachShader(program, vert);
glAttachShader(program, frag);
glLinkProgram(program);
glDeleteShader(vert);
glDeleteShader(frag);
return program;
}
int main() {
glfwInit();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
GLFWwindow* window = glfwCreateWindow(800, 600, "Triangle", nullptr, nullptr);
glfwMakeContextCurrent(window);
gladLoadGLLoader((GLADloadproc)glfwGetProcAddress);
float vertices[] = {
-0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f,
0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f,
0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f,
};
unsigned int VAO, VBO;
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);
glBindVertexArray(VAO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3 * sizeof(float)));
glEnableVertexAttribArray(1);
unsigned int shader = createShaderProgram("shaders/basic.vert", "shaders/basic.frag");
while (!glfwWindowShouldClose(window)) {
glClearColor(0.1f, 0.1f, 0.2f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
glUseProgram(shader);
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 3);
glfwSwapBuffers(window);
glfwPollEvents();
}
glDeleteVertexArrays(1, &VAO);
glDeleteBuffers(1, &VBO);
glDeleteProgram(shader);
glfwTerminate();
return 0;
}
まとめ
今回やったこと:
- 頂点データを定義
- VBO/VAOでGPUにデータを送る
- シェーダーを書いてコンパイル
-
glDrawArraysで描画
三角形が描けた瞬間、めちゃくちゃ感動する。
次回はシェーダーをもっと深掘り!
次回予告
Part4: シェーダー書いたらGPUと対話できた
uniform変数を使って、色を動的に変える!