C++でOpenGLを使ってゲーム用ライブラリを作った話 #0

# はじめに
[農工大アドベントカレンダー7日目の記事](https://qiita.com/advent-calendar/2024/tuat)です。

次回記事 [C++でOpenGLを使ってゲーム用ライブラリを作った話 #1](https://qiita.com/upiscium/private/120828a9d69115209845)

# この連載のリポジトリ
https://github.com/uPiscium/QiitaGameLib

今回は`Article0`フォルダの内容です。

# 宣伝
https://github.com/uPiscium/TerreateGraphics

今回の連載の元ネタにあたる、筆者が実際に作成したライブラリです。
気が向いたら覗いてみてください。(スターしてくれると筆者がとっても喜びます✨)

# TL;DR
[LearnOpenGL](https://learnopengl.com/)に従ってやっていけば大体できます。(全部英語ですが、Google先生やChatGPTとお話しして頑張りましょう...)

# 注意
この記事は[LearnOpenGL](https://learnopengl.com/)を参考にしていますが、**同じコードを実行しているわけでは無い**のでその点は注意してください。また、この手の記事はもっとクオリティの高いものがそこら中に転がっているのでそっちを読むことを推奨します。(こんな中身無い記事を読んでも...ね...?)
それでも良いよって人はぜひ読んでってください(^^)

# 経緯
突然ですがこれなんだかわかりますか？
![screen.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3816960/b056820c-f8af-9d5f-be8f-082fcfd4e01a.png)
そう、みんな大好きUnityのインスペクター画面ですね(白目)

ところでこの画面...**情報量多すぎませんか!!?**
どこいじったらどこが変わるんだかパッ見では分からないし、何かやりたいことがあったときもどこを変更すればいいか分かんなくて延々とネットをふわふわしないといけない...
これを見て私は「もっと色々わかりやすい、できれば**全てをソースコードベースで管理できる**ようなものが欲しいなぁ(絶賛開発中)」と思い、開発を始めてみました。

# 本記事の内容
- [OpenGLってなんですか](#OpenGLってなんですか)
- [今回使うライブラリたち](#今回使うライブラリたち)
- [とりあえず画面出したいよね](#とりあえず画面出したいよね)
- [図形描きたいよね](#図形描きたいよね)
- [まとめ](#まとめ)
- [コード全文](#コード全文)

# OpenGLってなんですか
そもそも今回使っていくOpenGLがなんなのかって言う話です。
ChatGPT君に聞いてみたら次のような答えが返って来ました〜

> \>\>\> OpenGLってなんですか？
>
> **OpenGL (Open Graphics Library)** は、2Dおよび3Dのグラフィックスを描画するための**クロスプラットフォームなAPI**です。OpenGLは、主にゲーム開発やシミュレーション、科学可視化、CADソフトウェアなどで使用され、ハードウェアアクセラレーションを利用してリアルタイムに描画を行います。
> ### 主な特徴
> 1. **クロスプラットフォーム対応**:
>    - Windows、macOS、Linuxなど、さまざまなOSで動作します
>    - AndroidやiOSなどのモバイルデバイスでも利用可能（モバイル版は「OpenGL ES」）
>
> 2. **低レベルAPI**:
>    - シェーダーやテクスチャなどの**低レベルなグラフィックス操作**を行うことができます
>    - 直接ハードウェアにアクセスするわけではなく、ドライバがハードウェアとの橋渡しを行います
>
> 3. **拡張可能**:
>    - ハードウェアベンダー（NVIDIA, AMDなど）が独自の拡張機能を提供し、最新技術をいち早く使用できるようになっています
>
> ### 基本的な構成
> OpenGLは、**ステートマシン**として設計されており、次のような主要なコンポーネントがあります：
>
> - **バッファ (Buffer)**:
>   - 頂点情報やインデックス情報などを保持するメモリ領域です
> - **シェーダー (Shader)**:
>   - GPU上で動作するプログラム。頂点シェーダーやフラグメントシェーダーが代表例です
> - **テクスチャ (Texture)**:
>   - 画像データを使用してオブジェクトの表面を飾るためのデータです
> - **レンダリングパイプライン**:
>   - 頂点の処理からピクセルの出力までの一連の処理の流れ

ChatGPT君すごいですね...(これ記事書く必要無いんじゃね？)
ChatGPT君が言うとおり、OpenGLはクロスプラットフォームなグラフィック制御フレームワークです。とりあえずOpenGLを使ってなんか作ればその部分だけはどのOSに持って行っても大体動きます。とっても嬉しいですね。次章からはいよいよ実際にコードを書いて目に見えるものを作って行きます！

# 今回使うライブラリたち
今回はこの2つで画面を出して図形を表示するところまでをとりあえずやっていこうと思います。あと、この記事ではライブラリのリンク設定をしたり、インクルードパスを設定したりするくだりは**省略します**。他のもっといい記事を読んで頑張りましょう。

## glfw
OpenGLの基本的な制御に利用します。色々なインタフェースを提供してくれているので便利です。

## glad
glfwのヘルパーライブラリです。OpenGLのバッファやシェーダーを制御する関数をいい感じに使えるようにするインタフェースを提供してくれます。これが無いと何もないまっさらな画面を眺めるか気合で関数ポインタを引っ張り出すとかいうようわからんことをする羽目になります。絶対に入れましょう。(これ以外にもglutやglewといったヘルパーライブラリを利用する場合もあります)

# とりあえず画面出したいよね
とりあえずこんな感じで画面出してみたいですよね。
![screen.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3816960/45307eaa-6749-1b79-1855-9450d6ff7891.png)
まあ、何もないただの画面なんて面白みの欠片も無いんですけどねw
最初はここから始めないとなんにも分かんなくなっちゃうんで辛抱強くやっていきましょう〜

とりあえず、次のコードをコピペして実行すると上の画面が出てきます。
```cpp
#include <glad/gl.h>    //  この2つのインクルードの順番を
#include <GLFW/glfw3.h> //      入れ替えるとエラーが起こるので注意
#include <iostream>

void framebuffer_size_callback(GLFWwindow *window, int width, int height);
void processInput(GLFWwindow *window);

int main() {
  // GLFWの初期化
  if (!glfwInit()) {
    std::cerr << "Failed to initialize GLFW" << std::endl;
    return -1;
  }

  // OpenGLのバージョンを設定 (3.3 Core Profile)
  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, "OpenGL Window", nullptr, nullptr);
  if (window == nullptr) {
    std::cerr << "Failed to create GLFW window" << std::endl;
    glfwTerminate();
    return -1;
  }
  glfwMakeContextCurrent(window);

  // GLADの初期化
  if (!gladLoadGL((GLADloadfunc)glfwGetProcAddress)) {
    std::cerr << "Failed to initialize GLAD" << std::endl;
    return -1;
  }

  // ビューポートの設定
  glViewport(0, 0, 800, 600);
  glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);

  // メインループ
  while (!glfwWindowShouldClose(window)) {
    // 入力処理
    processInput(window);

    // 背景色の設定 (0.2, 0.2, 0.2)
    glClearColor(0.2f, 0.2f, 0.2f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT);

    // バッファのスワップとイベントのポーリング
    glfwSwapBuffers(window);
    glfwPollEvents();
  }

  // GLFWの終了処理
  glfwTerminate();
  return 0;
}

// ウィンドウサイズが変更されたときのコールバック関数
void framebuffer_size_callback(GLFWwindow *window, int width, int height) {
  glViewport(0, 0, width, height);
}

// 入力処理
void processInput(GLFWwindow *window) {
  if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS) {
    glfwSetWindowShouldClose(window, true);
  }
}

```
各コードの説明はコメントの通りなんですが一応軽く解説します。
```cpp
// GLFWの初期化
if (!glfwInit()) {
  std::cerr << "Failed to initialize GLFW" << std::endl;
  return -1;
}
```
この部分ではglfwの初期化をやってます。これをやらないと後ろの方で出てくる
`glfw~()`みたいな名前の関数が呼び出せなくなります。忘れないようにしましょう！

```cpp
// OpenGLのバージョンを設定 (3.3 Core Profile)
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
```
ここではOpenGLのバージョン指定をやってます。まあ、いわゆる **お ま じ な い** と呼ばれているやつです。今は何も考えずにコピペで大丈夫です👍
(真面目に言うとバージョンによって利用できる機能に違いがあります。基本的に新しいもののほうが使える機能は多くなりますが、ハードウェアによっては対応してない場合もあるので気をつけましょう...)

```cpp
// ウィンドウの作成
GLFWwindow *window = glfwCreateWindow(800, 600, "OpenGL Window", nullptr, nullptr);
if (window == nullptr) {
  std::cerr << "Failed to create GLFW window" << std::endl;
  glfwTerminate();
  return -1;
}
glfwMakeContextCurrent(window);
```
ここでは画面を初期化しています。なんか色々と複雑そうですが一個づつ見ると意外と簡単です。
1. まず、`glfwCreateWindow(...)`関数で画面のオブジェクト(正確には構造体)を生成します。引数は順に`[幅, 高さ, 画面のタイトル, モニターオブジェクト, バッファを共有する別の画面]`です。最後の2つについては現状はnullptrで問題ないです
1. その後、生成した画面が有効であることを確認します。有効でない場合は`glfwTerminate()`関数でglfwの終了処理を行ってプログラムを終了します
(これを呼び出さないとエラーが発生したりするので忘れないようにしましょう〜)
1. 最後に`glfwMakeContextCurrent()`関数で生成した画面を操作する対象に設定して制御できるようにします
(これを呼び出さないと「変更が画面に反映されないよ〜」という悲しい状態に陥るのでしっかり呼び出しましょう〜)

```cpp
// GLADの初期化
if (!gladLoadGL((GLADloadfunc)glfwGetProcAddress)) {
  std::cerr << "Failed to initialize GLAD" << std::endl;
  return -1;
}
```
ここではglfwの関数ポインタを引っ張り出すためのエントリをgladのOpenGL関数ローダーに渡して`gl~()`みたいな名前の関数を初期化しています。これが失敗した場合はほとんどの機能が利用できないので、プログラムを終了します。
(これを呼び出さない状態だと、この後に出てくる`gl~()`系の関数を呼び出したときに`Segmentation fault`を起こしてブチ切れることになるので必ず呼び出しましょう。)

```cpp
// ビューポートの設定
glViewport(0, 0, 800, 600);
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
```
ここでは`glViewPort()`関数で画面に描画する範囲を設定しています。この部分を変更すると色のつく範囲が変わるので色々変えて見ると面白いです。(引数は順に`[左下のx座標, 左下のy座標, 幅, 高さ]`です。)
更にここでは画面のサイズが変更されたときに呼び出されるコールバックも設定しています。この他にもキーボードやマウスなどのコールバックもあるので気になる人は[glfwの画面に関するページ](https://www.glfw.org/docs/latest/window_guide.html)を確認してみると良いかも。

```cpp
// メインループ
while (!glfwWindowShouldClose(window)) {
  // 入力処理
  processInput(window);

  // 背景色の設定 (0.2, 0.2, 0.2)
  glClearColor(0.2f, 0.2f, 0.2f, 1.0f);
  glClear(GL_COLOR_BUFFER_BIT);

  // バッファのスワップとイベントのポーリング
  glfwSwapBuffers(window);
  glfwPollEvents();
}
```
この部分では描画ループを行っています。`glfwWindowShouldClose()`関数で画面が閉じられたかどうかを確認して、閉じられたらループを抜けるようにします。何かを描画したいときはこのループの中に処理を書きます。今回は次の3つの処理をしています。
1. ユーザーのキー入力を受け取って画面を閉じる処理
1. `glClearColor()`関数で背景を塗る色を設定してから、`glClear()`関数で画面のバッファを初期化します。今回は`GL_COLOR_BUFFER_BIT`を設定することで画面の色情報(RGBA値)を格納しているバッファを(R, G, B, A) = (0.2, 0.2, 0.2, 1.0)で初期化します。これは全ての画素に対して行われるので結果として画面全体が同じ色で塗りつぶされます
1. `glfwSwapBuffers()`で画面に表示するバッファと描画するバッファを入れ替えて描画結果を画面に反映します。(これを忘れると描画したのに画面変わらないという悲しいことが起こります...)
1. 最後に`glfwPollEvents()`でシステムイベントを処理します。(これを呼び出さないと画面が固まってなんの応答も返さなくなります。Windowsだと「アプリケーションは応答していません」と言われます)

```cpp
// GLFWの終了処理
glfwTerminate();
return 0;
```
`main()`関数の最後であるこの部分でglfwの終了処理をやって、プログラムを終了しています。

```cpp
// ウィンドウサイズが変更されたときのコールバック関数
void framebuffer_size_callback(GLFWwindow *window, int width, int height) {
  glViewport(0, 0, width, height);
}

// 入力処理
void processInput(GLFWwindow *window) {
  if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS) {
    glfwSetWindowShouldClose(window, true);
  }
}
```
最後に2つの関数について説明します。
まず、`framebuffer_size_callback()`関数についてですが、これは単純に画面サイズに合わせて描画する範囲を設定しているだけです。(上の方で紹介した`glViewPort()`関数を使っていい感じにしてます。)
`processInput()`関数は`glfwGetKey()`関数を使ってキーボードからのキー入力を受け取って、必要に応じて`glfwSetWindowShouldClose()`関数で画面を閉じています。今回の場合はEscキーが押されると画面が閉じるようになっています。

これで画面が出せるようになりました！

# 図形描きたいよね
図形を書くためには今までとは少し違った仕組みを利用する必要があります。
OpenGLには頂点バッファと呼ばれる図形の頂点情報を保存しておくためのバッファがあり、それを使うことで画面に図形を描画することができます。

じゃあ、これで図形が描画できるかというと...
それだけでは図形を書くことが**できません**！！！(面倒くさいね)

OpenGLでは頂点バッファに加えて、シェーダーと呼ばれる頂点をどのように画面に描画するかを設定する小さなプログラムが必要になります。
とりあえず面倒な座学は置いといて早速、追加するプログラムを見ていきましょう！

```cpp
// 頂点シェーダーのソースコード
const char* vertexShaderSource = R"(
#version 330 core
layout (location = 0) in vec3 aPos;
void main() {
    gl_Position = vec4(aPos, 1.0);
}
)";

// フラグメントシェーダーのソースコード
const char* fragmentShaderSource = R"(
#version 330 core
out vec4 FragColor;
void main() {
    FragColor = vec4(1.0, 0.5, 0.2, 1.0);
}
)";
```

これがさっき言っていたシェーダーのソースコードです。これを関数宣言の上の部分に追記します。

```diff_cpp
#include <glad/gl.h>    //  この2つのインクルードの順番を
#include <GLFW/glfw3.h> //      入れ替えるとエラーが起こるので注意
#include <iostream>

// 頂点シェーダーのソースコード
+ const char* vertexShaderSource = R"(
+ #version 330 core
+ layout (location = 0) in vec3 aPos;
+ void main() {
+     gl_Position = vec4(aPos, 1.0);
+ }
+ )";

// フラグメントシェーダーのソースコード
+ const char* fragmentShaderSource = R"(
+ #version 330 core
+ out vec4 FragColor;
+ void main() {
+     FragColor = vec4(1.0, 0.5, 0.2, 1.0);
+ }
+ )";

void framebuffer_size_callback(GLFWwindow *window, int width, int height);
void processInput(GLFWwindow *window);

int main() {
...
```
シェーダーの詳しい説明については今回の記事では省略しますが、簡単に言うとC言語に似た言語で`main()`関数に書いた処理が実行されます。`main()`関数の中に書く内容を変えることで図形の位置や色を変更することができます。(シェーダーについて気になる人は "ジェネレーティブアート" 等の単語で検索すると色々見つかって面白いかも)
今回のソースコードでは`FragColor = vec4(1.0, 0.5, 0.2, 1.0);`とすることで橙色の図形を表示するようにしています。

続けてシェーダーを初期化するための関数宣言を追加します。
```diff_cpp
...
void framebuffer_size_callback(GLFWwindow *window, int width, int height);
void processInput(GLFWwindow *window);
+ unsigned int createShaderProgram(const char* vertexSource, const char* fragmentSource);

int main() {
...
```

次に、図形情報を生成してOpenGLに渡すコードと、シェーダーを生成するコードを`main()`関数内に追記します。
```diff_cpp
    ...
    // ビューポートの設定
    glViewport(0, 0, 800, 600);
    glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);

    // シェーダープログラムの作成
+   unsigned int shaderProgram = createShaderProgram(vertexShaderSource, fragmentShaderSource);

    // 三角形の頂点データ
+   float vertices[] = {
+        0.0f,  0.5f, 0.0f,  // 上
+       -0.5f, -0.5f, 0.0f,  // 左下
+        0.5f, -0.5f, 0.0f   // 右下
+   };

    // VAOとVBOの作成
+   unsigned int VAO, VBO;
+   glGenVertexArrays(1, &VAO);
+   glGenBuffers(1, &VBO);

    // VAOの設定
+   glBindVertexArray(VAO);

    // VBOの設定
+   glBindBuffer(GL_ARRAY_BUFFER, VBO);
+   glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

    // 頂点属性の設定
+   glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
+   glEnableVertexAttribArray(0);

    // メインループ
    while (!glfwWindowShouldClose(window)) {
    ...
```
それぞれの部分についてざっくりと説明すると
1. 最初の`unsigned int shaderProgram = ...`の行で図形を描画するためのシェーダーを用意して、続く部分で三角形を`float`型の配列として定義しています。(定義されている配列は1次元ですが、内容としては`(x, y, z)`の座標が3組格納されている2次元配列が1次元になったと思って見てください。)
1. その次の部分で`VAO`と`VBO`の初期化と生成を行っています
1. 生成した`VAO`を操作する対象に設定します
1. 生成した`VBO`を操作する対象に設定して、手前で定義した三角形の頂点情報を転送します。(引数は順に`[転送先のVBO, 転送するデータのサイズ(byte), 実際のデータ, データの使われ方]`です。)
`データの使われ方`の詳しい説明については[Khronosレジストリ](https://registry.khronos.org/OpenGL-Refpages/gl4/html/glBufferData.xhtml)の**Description**を確認してください。今回は送ったあとに特にデータの変更等はしないので`GL_STATIC_DRAW`で大丈夫です
1. `VBO`の中の情報がどんなフォーマットで格納されているかをOpenGLに伝えます。(引数は順に`[頂点属性のインデックス, 構成している要素の個数, 要素の型, 正規化されているか, ストライド(byte), オフセット(byte)]`です。)
インデックスは今回の場合は0番目の頂点属性(←頂点の情報を構成する位置とか色とかのこと)なので`0`、構成している要素は`(x, y, z)`なので`3`、正規化はしていないので`GL_FALSE`、ストライド(1つの頂点の情報の長さ)は1つの頂点につき`(float, float, float)`の情報を持っているから`3 * sizeof(float)`、オフセット(バッファ先頭からの相対位置)はないので`0`となっています
1. 0番目の頂点属性を有効化します。(引数は`[有効化したい頂点属性のインデックス]`です。今回は手前で`0`番を設定したので`0`を指定して有効化します。)

次にしれっと出てきた`VAO`と`VBO`について軽く触れておきます。

### VAO
`VAO`というのは複数の`VBO`をまとめて管理するためのオブジェクトで複数の`VBO`を利用する場合に使うと便利になります。(今回の場合は`VBO`を1つしか利用していないのであんまり意味がないです。) また、`IBO`とかいうのもあって、こいつもくっつけておくといい感じに描画で楽ができます。

### VBO
`VBO`というのは図形の頂点情報(位置や色など)を格納して置くためのバッファを管理するオブジェクトでこれが無いと図形を描画することができません。

-----------
次に、図形を書くための色々を描画ループの中に追加して、描画ループのあとにOpenGLのオブジェクトの後始末を追加します。
```diff_cpp
...
    // 背景色の設定とクリア
    glClearColor(0.2f, 0.2f, 0.2f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT);

    // シェーダープログラムの使用
+   glUseProgram(shaderProgram);

    // 三角形の描画
+   glBindVertexArray(VAO);
+   glDrawArrays(GL_TRIANGLES, 0, 3);

    // バッファのスワップとイベントのポーリング
    glfwSwapBuffers(window);
    glfwPollEvents();
  }

  // リソースの解放
+ glDeleteVertexArrays(1, &VAO);
+ glDeleteBuffers(1, &VBO);
+ glDeleteProgram(shaderProgram);
...
```
まず、`glUseProgram()`関数を使って生成したシェーダーを描画する際に利用するようにOpenGLに伝えます。そして、さっきも出てきた`glBindVertexArray()`で生成した`VAO`を操作対象(今回は描画対象)に設定して、`glDrawArrays()`関数で描画する頂点の数を指定して描画を実行します。(引数は順に`[描画する図形の種類, オフセット, 描画する頂点の数]`です。)
最後に、生成したOpenGLのオブジェクトを消去してリソースを解放します。

-----
最後にちょっと前の部分で追加したシェーダー生成用関数を追記します。
```diff_cpp
// シェーダープログラムの作成関数
unsigned int createShaderProgram(const char* vertexSource, const char* fragmentSource) {
    // 頂点シェーダーのコンパイル
    unsigned int vertexShader = glCreateShader(GL_VERTEX_SHADER);
    glShaderSource(vertexShader, 1, &vertexSource, nullptr);
    glCompileShader(vertexShader);

    // コンパイルエラーチェック
    int success;
    char infoLog[512];
    glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);
    if (!success) {
        glGetShaderInfoLog(vertexShader, 512, nullptr, infoLog);
        std::cerr << "ERROR::VERTEX_SHADER::COMPILATION_FAILED\n" << infoLog << std::endl;
    }

    // フラグメントシェーダーのコンパイル
    unsigned int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
    glShaderSource(fragmentShader, 1, &fragmentSource, nullptr);
    glCompileShader(fragmentShader);

    // コンパイルエラーチェック
    glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &success);
    if (!success) {
        glGetShaderInfoLog(fragmentShader, 512, nullptr, infoLog);
        std::cerr << "ERROR::FRAGMENT_SHADER::COMPILATION_FAILED\n" << infoLog << std::endl;
    }

    // シェーダープログラムのリンク
    unsigned int shaderProgram = glCreateProgram();
    glAttachShader(shaderProgram, vertexShader);
    glAttachShader(shaderProgram, fragmentShader);
    glLinkProgram(shaderProgram);

    // リンクエラーチェック
    glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
    if (!success) {
        glGetProgramInfoLog(shaderProgram, 512, nullptr, infoLog);
        std::cerr << "ERROR::SHADER_PROGRAM::LINKING_FAILED\n" << infoLog << std::endl;
    }

    // シェーダーオブジェクトの削除
    glDeleteShader(vertexShader);
    glDeleteShader(fragmentShader);

    return shaderProgram;
}
```
このコードについては今回の記事では解説しません。(力尽きました...はい...ﾕﾙｼﾃ)

# まとめ
今回はOpenGLを利用した画面の表示と簡単な図形の描画について扱いました。中間テストとか色々あるのでゴミみたいな内容になってしまいましたが、気が向いたら続編を出すかも。(シェーダー生成関数とかをしっかり解説しないとやばい...)
それではまた次の記事でお会いしましょう。(^^)ﾉｼ

# コード全文
<details><summary>main.cpp</summary>

```cpp
#include <glad/gl.h>
#include <GLFW/glfw3.h>
#include <iostream>

// 関数宣言
void framebuffer_size_callback(GLFWwindow *window, int width, int height);
void processInput(GLFWwindow *window);
unsigned int createShaderProgram(const char *vertexSource,
                                 const char *fragmentSource);

// 頂点シェーダーのソースコード
const char *vertexShaderSource = R"(
#version 330 core
layout (location = 0) in vec3 aPos;
void main() {
    gl_Position = vec4(aPos, 1.0);
}
)";

// フラグメントシェーダーのソースコード
const char *fragmentShaderSource = R"(
#version 330 core
out vec4 FragColor;
void main() {
    FragColor = vec4(1.0, 0.5, 0.2, 1.0);
}
)";

int main() {
  // GLFWの初期化
  if (!glfwInit()) {
    std::cerr << "Failed to initialize GLFW" << std::endl;
    return -1;
  }

  // OpenGLのバージョン設定 (3.3 Core Profile)
  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, "OpenGL Triangle", nullptr, nullptr);
  if (!window) {
    std::cerr << "Failed to create GLFW window" << std::endl;
    glfwTerminate();
    return -1;
  }
  glfwMakeContextCurrent(window);

  // GLADの初期化
  if (!gladLoadGL((GLADloadfunc)glfwGetProcAddress)) {
    std::cerr << "Failed to initialize GLAD" << std::endl;
    return -1;
  }

  // ビューポートの設定
  glViewport(0, 0, 800, 600);
  glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);

  // シェーダープログラムの作成
  unsigned int shaderProgram =
      createShaderProgram(vertexShaderSource, fragmentShaderSource);

  // 三角形の頂点データ
  float vertices[] = {
      0.0f,  0.5f,  0.0f, // 上
      -0.5f, -0.5f, 0.0f, // 左下
      0.5f,  -0.5f, 0.0f  // 右下
  };

  // VAOとVBOの作成
  unsigned int VAO, VBO;
  glGenVertexArrays(1, &VAO);
  glGenBuffers(1, &VBO);

  // VAOの設定
  glBindVertexArray(VAO);

  // VBOの設定
  glBindBuffer(GL_ARRAY_BUFFER, VBO);
  glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

  // 頂点属性の設定
  glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void *)0);
  glEnableVertexAttribArray(0);

  // メインループ
  while (!glfwWindowShouldClose(window)) {
    // 入力処理
    processInput(window);

    // 背景色の設定とクリア
    glClearColor(0.2f, 0.2f, 0.2f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT);

    // シェーダープログラムの使用
    glUseProgram(shaderProgram);

    // 三角形の描画
    glBindVertexArray(VAO);
    glDrawArrays(GL_TRIANGLES, 0, 3);

    // バッファのスワップとイベントのポーリング
    glfwSwapBuffers(window);
    glfwPollEvents();
  }

  // リソースの解放
  glDeleteVertexArrays(1, &VAO);
  glDeleteBuffers(1, &VBO);
  glDeleteProgram(shaderProgram);

  // GLFWの終了処理
  glfwTerminate();
  return 0;
}

// ウィンドウサイズが変更されたときのコールバック関数
void framebuffer_size_callback(GLFWwindow *window, int width, int height) {
  glViewport(0, 0, width, height);
}

// 入力処理
void processInput(GLFWwindow *window) {
  if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS) {
    glfwSetWindowShouldClose(window, true);
  }
}

// シェーダープログラムの作成関数
unsigned int createShaderProgram(const char *vertexSource,
                                 const char *fragmentSource) {
  // 頂点シェーダーのコンパイル
  unsigned int vertexShader = glCreateShader(GL_VERTEX_SHADER);
  glShaderSource(vertexShader, 1, &vertexSource, nullptr);
  glCompileShader(vertexShader);

  // コンパイルエラーチェック
  int success;
  char infoLog[512];
  glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);
  if (!success) {
    glGetShaderInfoLog(vertexShader, 512, nullptr, infoLog);
    std::cerr << "ERROR::VERTEX_SHADER::COMPILATION_FAILED\n"
              << infoLog << std::endl;
  }

  // フラグメントシェーダーのコンパイル
  unsigned int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
  glShaderSource(fragmentShader, 1, &fragmentSource, nullptr);
  glCompileShader(fragmentShader);

  // コンパイルエラーチェック
  glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &success);
  if (!success) {
    glGetShaderInfoLog(fragmentShader, 512, nullptr, infoLog);
    std::cerr << "ERROR::FRAGMENT_SHADER::COMPILATION_FAILED\n"
              << infoLog << std::endl;
  }

  // シェーダープログラムのリンク
  unsigned int shaderProgram = glCreateProgram();
  glAttachShader(shaderProgram, vertexShader);
  glAttachShader(shaderProgram, fragmentShader);
  glLinkProgram(shaderProgram);

  // リンクエラーチェック
  glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
  if (!success) {
    glGetProgramInfoLog(shaderProgram, 512, nullptr, infoLog);
    std::cerr << "ERROR::SHADER_PROGRAM::LINKING_FAILED\n"
              << infoLog << std::endl;
  }

  // シェーダーオブジェクトの削除
  glDeleteShader(vertexShader);
  glDeleteShader(fragmentShader);

  return shaderProgram;
}

```
</details>